Skip to content

类关系、设计模式、范式

更新: 2025/2/24 字数: 0 字 时长: 0 分钟

类关系

在写代码的过程中,我们创建的类之间可能存在一定的关系,具体关系有如下三种:

  • 类的继承关系 is-a,例如 a stdent is a person
  • 类的关联关系 has-a,例如 a stdent has an Id-Card
  • 类的依赖关系 use-a,例如 a stdent use a vehicle

类的继承

类的继承:在已有的类的基础上扩展出新类,这个新类继承了已有类中所有的属性和方法。在前面我们提到 Python 中所有的类都直接或间接地继承了 object 类,因此 Python 中所有的类都可以直接使用 object 类中所有的属性和方法。

python
# 继承类的时候,在声明子类的括号里添加已存在的父类即可。
class 子类(父类1, 父类2...):
    '''类的说明'''
    属性
    方法
  • 父类:也叫基类,指被继承的类。如果没有父类,需要连同 () 一起省略掉,直接默认继承 object 类。
  • 子类:也叫派生类,指从父类基础上扩展出的新类。在未声明父类的情况下,都会默认继承 object 类,但一般省略不写。

继承方式

类的继承方式主要有这么以下三种:

  • 单类继承:一个类只有一个父类。
python
class A:
    name = 'A'

class C(A):  # 注释:声明C类继承了A类
    pass

# 输出父类的类变量
print(C.name)            # 输出:A。注释:输出父类的name类变量。

# 使用isinstance判断对象所属的类
a = A()
print(isinstance(a, A))  # 输出:True。注释:对象a属于A类。
print(isinstance(a, C))  # 输出:False。注释:对象a不属于C类。
c = C()
print(isinstance(c, A))  # 输出:True。注释:对象c属于A类。
print(isinstance(c, C))  # 输出:True。注释:对象c属于C类。
  • 多类继承:一个类同时继承多个类。
python
class A:
    name = 'A'

class B:
    name = 'B'
    age = 18

class C(A, B):  # 注释:声明C类继承的第一个父类是A类,继承的第二个父类是B类。
    pass

# 输出父类的类变量
print(C.name)   # 输出:A。注释:当继承的多个类有相同的类变量时,默认继承第一个类中类变量。
print(C.age)    # 输出:18。注释:因为A类中没有age类变量,因此输出B类中的age类变量。

# 使用isinstance判断对象所属的类
a = A()
print(isinstance(a, A))  # 输出:True。注释:对象a属于A类。
print(isinstance(a, B))  # 输出:False。注释:对象a不属于B类。
print(isinstance(a, C))  # 输出:False。注释:对象a不属于C类。
c = C()
print(isinstance(c, A))  # 输出:True。注释:对象c属于A类。
print(isinstance(c, B))  # 输出:True。注释:对象c属于B类。
print(isinstance(c, C))  # 输出:True。注释:对象c属于C类。

注意

如果不是必须使用多类继承的场景下,请尽量使用单类继承。如果必须使用多类继承,则尽量避免形成“菱形继承结构”(例如 B 类、C 类都继承 A 类,而 D 类又同时继承了 B 类、C 类),否则代码的可读性和可理解性都会变得非常糟糕。

继承内容

子类继承父类时,具体的继承内容说明如下:

  1. 类变量、实例属性:类变量(普通类变量、私有类变量)是类级别的,而实例属性是对象级别的。无论是类变量还是实例属性,它们都不是“继承”的,而是被类的实例所“拥有”的。因此当子类继承父类后,子类可以拥有父类的类变量和实例属性。
python
class Per:
    number = 61
    _private = 'private'

    def __init__(self):
        self.name = 'Tim'

# Student类继承Person类
class Student(Per):
        pass

# 子类调用父类的变量
print(Student.number)    # 输出:61。注释:调用父类的类变量。
print(Student._private)  # 输出:private。注释:调用父类中的私有类变量。

# 子类对象调用父类的变量和对象属性
stu = Student()
print(stu.number)        # 输出:61。注释:调用父类的类变量。
print(stu._private)      # 输出:private。注释:调用父类的私有类变量。
print(stu.name)          # 输出:Tim。注释:调用父类的实例属性。
  1. __slots__ 属性约束:在前面我们曾说过在类中定义了 __slots__ 属性约束后,那么该类的实例对象就不能再使用 __dict__ 属性了,但这对于继承父类的子类却不起作用。因为当子类继承父类的 __slots__ 属性约束时,同时也会自动创建 __dict__ 内置属性用来动态拓展属性。
python
class Per:
    __slots__ = ('name')

    def __init__(self):
        self.name = 'Tim'

# Student类继承Person父类
class Student(Per):
        pass

# 子类对象调用父类对象的属性
stu = Student()
print(stu.name)          # 输出:Tim。注释:调用父类的实例属性。
print(stu.__slots__)     # 输出:('name')。注释:调用父类的属性约束。
print(stu.__dict__)      # 输出:{}。注释:子类对象创建__dict__动态拓展属性。
stu.age = 18             # 注释:将age属性添加到__dict__动态拓展属性当中。
print(stu.age)           # 输出:18
print(stu.__dict__)      # 输出:{'age': 18}
  1. 对象方法、类方法、静态方法:对象方法(普通方法、私有方法)定义了对象的行为,可以被子类继承。而类方法、静态方法都是定义在类上的,可以被子类继承。
python
class Per:
    number = 61

    def __init__(self):
        self.name = 'Tim'

    def eat(self):
        print(f'{self.name} eat')

    def _play(self):
        print(f'{self.name} play')

    @classmethod
    def get_number(cls):
        print(f'count {cls.number}')

    @staticmethod
    def hurt_earth():
        print('hurt earth')

# Student类继承Person父类
class Student(Per):
        pass

# 子类调用父类的属性、方法
Student.get_number()  # 输出:count 61。注释:调用父类中的类方法。
Student.hurt_earth()  # 输出:hurt earth。注释:调用父类中的静态方法。

# 子类对象调用父类对象的属性、方法
stu = Student()
stu.eat()             # 输出:Tim eat。注释:调用父类中的对象方法。
stu._play()           # 输出:Tim play。注释:调用父类中的私有对象方法。
stu.get_number()      # 输出:count 61。注释:调用父类中的类方法。
stu.hurt_earth()      # 输出:hurt earth。注释:调用父类中的静态方法。

警告

可以看到,继承是实现代码复用的一种手段,但是千万不要滥用继承。

重写方法

重写方法(Override Method):在子类中改写从父类继承下来的对象方法。

  1. 调用过程:先看当前类是否有此方法,没有才查看父类有没有此方法,父类没有就看父类的父类有没有,直到找到基类 object 为止。
  2. 部分重写:重写过程中保留父类方法的功能,只需要在重写的方法中添加 super().父类方法() 即可保留父类方法的功能。
  3. 全部重写:根据方法的调用过程,调用方法时会最先查看当前类,所以将子类的方法命名为和父类中要被重写方法一样的名称,即可实现对父类方法的重写。
  4. **保持一致:**重写后的子类方法当中的形参列表要和父类的形参列表保持一致。例如,父类形参中有关键字 *args**kwargs 参数,那么子类重写后的方法中也应该有关键字 *args**kwargs 参数。
python
class Animal:
    def __init__(self):
        self.age = 3

    def shot(self):
        print('叫唤')

    def eat(self):
        print('吃东西')


class Dog(Animal):  # 注释:Dog类继承Animal类。
    look = '看家'

    def __init__(self):
        super().__init__()  # 注释:保留父类对象属性。
        self.sex = 'M'      # 注释:子类添加'sex'属性。

    def shot(self):     # 注释:完全重写父类的shot方法。
        print('汪汪汪~')

    def eat(self):      # 注释:部分重写父类eat方法。
        super().eat()   # 注释:保留父类的eat方法。
        print('吃骨头')  # 注释:添加'吃骨头'功能。


# Animal类创建对象
an = Animal()  # 注释:创建an父类对象。
print(an.age)  # 输出:3
an.shot()      # 输出:叫唤
an.eat()       # 输出:吃东西

# Dog类创建对象
dog = Dog()     # 注释:创建dog子类对象。
print(dog.age)  # 输出:3。注释:子类对象使用父类的对象属性age。
print(dog.sex)  # 输出:M。注释:子类对象独有的对象属性sex。
dog.shot()      # 输出:汪汪汪~
dog.eat()       # 输出:吃东西 吃骨头

另外,在 PyCharm 中可以看到 Dog 类的对象方法的左侧都有一个圆点加一个向上的箭头,这表示该对象方法在父类 Animal 中有,而且在当前类中进行了重写。图片如下:

QQ截图20230412165200

重要

子类可以使用父类的属性和方法,但父类不能使子类的属性和方法,所以子类一定是比父类更强大的,任何时候都可以用子类对象去替代父类对象。

多态行为

多态(Polymorphism)就是给不同的对象发出同样的消息,不同的对象执行了不同的行为。在重写方法的过程中,不同的子类对父类的同一个方法给出不同的实现版本,那么该方法运行时就会表现出多态行为。而实现多态最重要的一步就是子类对父类方法的重写,给出自己的实现版本

类的关联

类的关联:把一个类的对象作为另外一个类对象的属性

python
class Computer:
    pass

class Student:
    def __init__(self):
        # 将Computer类对象赋值给Student类对象的computer属性,那么Student类和Computer类之间就是单向的关联关系。
        self.computer = Computer()

类的依赖

类的依赖:一个类的对象作为另外一个类对象方法的参数或返回值

python
class Vehicle:
    """交通工具"""
    pass

class Person:
    def drive(self, vehicle):
        pass

p = Person()
# Vehicle类对象作为Person类对象方法的参数
p.drive(vehicle=Vehicle())

这里给出一个综合了类的继承、类的关联和类的依赖这三种关系的案例,代码如下:

python
class Vehicle:
    """交通工具"""
    pass

# Horse类继承了Vehicle类(继承关系)
class Horse(Vehicle):
    """马"""
    pass

class Follower:
    """徒弟"""
    pass

class MrTang:
    """唐三藏"""
    def __init__(self):
        # Follower类对象作为MrTang类对象的属性(关联关系)
        self.followers = Follower()

    def drive(self, vehicle):
        pass

tang = MrTang()
# Vehicle类对象作为MrTang类对象方法的参数(依赖关系)
tang.drive(vehicle=Vehicle())

设计模式

设计模式(Design pattern)是一套被反复使用的代码设计经验总结,其目的是为了让代码更容易被他人理解、保证代码可靠性、程序的重用性。在项目中合理地运用设计模式可以完美地解决很多问题,每种模式都描述了一个不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。

单例模式

前面我们提到了一个类可以新建无数个对象,但在单例模式当中一个类从始至终只创建一个对象实例,这也是一种常用的软件设计模式。

实现单例

首先,我们先回顾一下类创建对象的 __new__ 方法,代码如下:

python
class Earth(object):

    # 创建对象方法
    def __new__(cls):
        return object.__new__(cls)

print(Earth() is Earth())  # 输出:False。注释:两次调用__new__方法返回的不是同一个对象。

现在我们来实现单例模式,在类中添加一个 _instance 私有类变量,其默认值为 None,用来保存第一次创建的对象,代码案例如下:

python
class Earth:
    _instance = None
    
    def __new__(cls):
        # 如果_instance布尔值为False,说明是第一次创建对象。
        if not cls._instance:
        	# 执行object.__new__(cls)创建对象,赋值给私有字段_instance。
            cls._instance = object.__new__(cls)
            # 返回新建对象
            return cls._instance
        # 如果_instance布尔值为True,说明对象已存在直接返回。
        return cls._instance

print(Earth() is Earth())  # 输出:True。注释:两次调用__new__方法返回的都是同一个对象。

控制属性

在单例模式的基础上,我们还可以控制单例模式的实例对象属性,具体有如下两种形式:

  • 属性值改变:实例对象的属性值可以被多次赋值,每次的赋值会覆盖现有的属性值。
python
class Earth:
    _instance = None

	# 定义__init__方法,name形参接收实参。
    def __init__(self, name):
        self.name = name

	# 多传了name参数,这里也要添加name形参。
    def __new__(cls, name):
        if cls._instance == None:
            cls._instance = object.__new__(cls)
            return cls._instance
        return cls._instance

a = Earth('地球')
print(a.name, id(a))  # 输出:地球 53131512912
b = Earth('火星')
print(b.name, id(b))  # 输出:火星 53131512912
print(a.name, id(a))  # 输出:火星 53131512912
  • 属性值不变:实例对象的属性值只能被赋值一次,后面的赋值不会改变第一次的赋值。
python
class Earth:
    _instance = None
    # 新增私有字段_first
    _first = True

    def __init__(self, name):
    	# 如果_first为True,说明是第一次初始化。
        if self._first == True:
            self._first = False
            self.name = name

    def __new__(cls, name):
        if cls._instance == None:
            cls._instance = object.__new__(cls)
            return cls._instance
        return cls._instance

a = Earth('地球')
print(a.name, id(a))  # 输出:地球 114167941496
b = Earth('火星')
print(b.name, id(b))  # 输出:地球 114167941496
print(a.name, id(a))  # 输出:地球 114167941496

建议

通过上面的案例可以看出,单例和属性控制都是通过私有字段变量保存第一次赋值的对象或属性再加以后期的判断来实现的。

工厂模式

“工厂模式”也称之为“静态工厂模式”,它提供了一种创建对象而不对客户端暴露创建逻辑的最佳方式,这也是一种常用来创建对象的软件设计模式。

创建对象

首先,我们创建一个 Student 学生类,代码如下:通过姓名 、年龄这两项基本信息创建一个学生对象,这两个信息就是所谓的创建逻辑。

python
class Student:
    def __init__(self, name, age):
        self.name=name
        self.age=age
 
student=Student('张三',23)

建议

在实际中,可能需要更多知道的信息,如身高、性别等,才能完全创建一个学生实例,但在不知道这些信息的情况下创建一个学生实例,这就是工厂模式要做的事情。

简单工厂

现在,我们创建一个 Factory 工厂类,里面封装了其他类创建对象的逻辑,代码如下:在创建某一个图形类的时候,根本不关心这个类的构造函数到底是什么,只需要告诉工厂,创建一个什么类即可,除此以外不需要知道任何的额外信息,这就是所谓的“隐藏创建类的代码逻辑”

python
import math

class Circle:
    """圆"""
    def Area(self, radius):
        return int(math.pow(radius, 2) * math.pi)

class Rectangle:
    """长方形"""
    def Area(self, length, width):
        return int(2 * length * width)

class Triangle:
    """三角形"""
    def Area(self, length, height):
        return int(length * height / 2)

class Factory:
    """工厂类"""
    def create(self, name):
        if name == 'Circle':
            return Circle()
        elif name == 'Rectangle':
            return Rectangle()
        elif name == 'Triangle':
            return Triangle()
        else:
            return None

cir = Factory().create('Circle')       # 注释:创建圆类
print(f'圆的面积是{cir.Area(2)}')        # 输出:圆的面积是12
rec = Factory().create('Rectangle')    # 注释:创建长方形类
print(f'长方形的面积是{rec.Area(2, 3)}')  # 输出:长方形的面积是12
tri = Factory().create('Triangle')     # 注释:创建三角形类
print(f'三角形的面积是{tri.Area(2, 3)}')  # 输出:三角形的面积是3

重要

工厂模式就是一个类里面封装了其他类创建对象的逻辑。作为一种创建其他类对象的工作模式,非常适合创建复杂对象,在任何需要生成复杂对象的地方都可以使用。有一点需要注意的是,使用工厂模式,就需要引入一个工厂类,对于简单对象,特别是只需要通过 __new__ 就可以完成创建的对象,无需使用工厂模式,否则会增加系统的复杂度。

编程范式

到这里我们就学习完了 Python 中的“函数”和“类”,这两种封装代码的方式对应了两种主流的编程范式,也是 Python 所支持的两种编程范式,它们分别是“面向函数编程”和“面向对象编程”。

面向函数编程

面向函数编程(Functional Programming,FP):旨在用一个个函数去解决问题或实现功能。

  • 核心:函数是一等公民,意味着函数在面向函数编程中享有与其他数据类型(如整数、字符串、列表等)同等的地位,它即可以作为参数传递给其他函数,也可以作为其他函数的返回值。

  • 优势:支持闭包和高阶函数,拥有更好的模块性、可测试性、并发支持和代码可重用性。

  • 劣势:所有的数据都是不可变的,严重占据运行资源,导致运行速度也不够快。

面向对象编程

面向对象编程(Object-Oriented Programming,OOP):将现实世界中的事物抽象成对象的编程方法。

  • 核心:基于对象的概念,以类作为对象的模板,把类和继承作为构造机制,以对象为中心,把问题看作由对象的属性与对象所进行的行为组成。

    • 抽象(Abstraction):提取共性的过程(定义类就是一个抽象的过程,需要做数据抽象和行为抽象)。
    • 封装(Encapsulation):把数据和操作数据的函数从逻辑上组装成一个整体(对象)。用更专业的话讲叫,隐藏实现细节,暴露简单的调用接口。
    • 继承(Inheritance):在已有的类的基础上扩展出新类的过程,实现对代码的复用。
    • 多态(Polymorphism):给不同的对象发出同样的消息,不同的对象执行了不同的行为。
  • 优势:利用面向对象编程的四大特征,可以很方便的写出结构清晰、易于理解和维护的代码,特别是在处理复杂系统和大型项目时。

  • 劣势:许多人为了面向对象而面向对象,给后期代码维护带来很多麻烦。

总结范式区别

面向函数编程和面向对象编程的区别和适用场景如下:

  • 对于面向函数编程来说,更适合于需要高度并发、数据密集或需要高度函数式抽象的场景,如数据处理、科学计算和 Web 开发中的某些部分。
  • 对于面向对象编程来说,更适用于需要模拟现实世界对象、处理复杂关系和状态的场景,如企业级应用开发、游戏开发和图形界面设计等。

总的来说,面向函数编程和面向对象编程各有其优势和适用场景。在实际开发中,根据项目的具体需求和团队的熟悉程度,可以选择合适的编程范式或结合使用两者。