Skip to content

面向对象

约 1549 个字 201 行代码 预计阅读时间 8 分钟

符合 Python 风格的类型

创建对象

得益于Python数据类型,自定义类型的行为可以像内置类型那样自然,实现如此行为,靠的不是继承,而是鸭子类型:只需要按照预定行为实现对象所需方法即可。比如:

# 这是一个二维向量,实现了非常多的特殊方法
from array import array
import math

class Vector2d:
    # 用于支持结构化模式匹配
    __match_args__ = ('x','y')

    # 类属性,用于在实例与字节序列之间的转换时使用
    typecode = 'd'

    def __init__(self,'x','y'):
        self.__x=float(x)
        self.__y=float(y)

    # property 装饰器用于将一个方法转换为属性
    @property
    def x(self):
        return self.__x

    @property
    def y(self):
        return self.__y

    # 把实例对象变成可迭代对象,这样才能拆包
    def __iter__(self):
        return (i for i in (self.x,self.y))

    # !r 是格式化字符串中的一种语法,用于调用对象的 __repr__() 方法
    def __repr__(self):
        class_name = type(self).__name__
        return '{}({!r},{!r})'.format(class_name,*self)

    def __str__(self):
        return str(tuple(self))

    def __bytes__(self):
        return (bytes([ord(self.typecode)])
                +bytes(array(self.typecode,self)))

    def __eq__(self,other):
        return tuple(self) == tuple(other)

    # 计算模长
    def __abs__(self):
        return math.hypot(self.x,self.y)

    def __bool__(self):
        return bool(abs(self))

    def angle(self):
        return math.atan2(self.y,self.x)

    # 格式化输出
    def __format__(self,fmt_spec=''):
        # 如果以 'p' 结尾,表示需要使用极坐标格式
        if fmt_spec.endswith('p'):
            # 去掉最后一个字符 'p'
            fmt_spec = fmt_spec[:-1]
            coords = (abs(self),self.angle())
            outer_fmt = '<{}, {}>'
        else:
            coords = self
            outer_fmt = '({}, {})'
        components = (format(c,fmt_spec) for c in coords)
        return outer_fmt.format(*components)

    # classmethod用于定义操作类而不是操作实例的方法
    @classmethod
    def frombytes(cls,octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        # 创建一个新实例
        return cls(*memv)

“私有”和“保护”

在Python中,类中的元素(如属性和方法)默认是公有的,即可以从类的外部直接访问。Python没有严格的访问控制机制(如C++中的private或protected),但通过命名约定和特殊语法,可以实现类似“私有”的效果: * 如果一个属性或方法的名字以单下划线(_)开头,例如 _name,它被视为受保护的(protected)。这是一种约定,表示该成员仅供内部使用或子类使用,但外部仍然可以访问。 * 如果一个属性或方法的名字以双下划线(__)开头,例如 __name,它会被Python解释器进行名称改写(name mangling),变成 _类名__name 的形式。这种改写使得该成员在外部难以直接访问,从而实现类似“私有”的效果。

class MyClass:
    def __init__(self):
        self.public_var = "I am public"  # 公有属性
        self._protected_var = "I am protected"  # 受保护属性
        self.__private_var = "I am private"  # 私有属性

    def public_method(self):
        print("This is a public method")

    def _protected_method(self):
        print("This is a protected method")

    def __private_method(self):
        print("This is a private method")

obj = MyClass()

# 访问公有成员
print(obj.public_var)  # 正常访问
obj.public_method()    # 正常调用

# 访问受保护成员(不推荐,但可以访问)
print(obj._protected_var)  # 可以访问,但不推荐
obj._protected_method()    # 可以调用,但不推荐

# 访问私有成员(会报错)
# print(obj.__private_var)  # 报错:AttributeError
# obj.__private_method()    # 报错:AttributeError

# 通过名称改写访问私有成员
print(obj._MyClass__private_var)  # 可以访问
obj._MyClass__private_method()    # 可以调用

接口和抽象基类

分类

在不同编程语言中,接口定义和使用方式不尽相同。从Python 3.8开始,有四种方式: 1. 鸭子类型:Python自诞生以来默认使用的类型实现方式,对象的类型由它的行为(即它具有的方法和属性)决定,而不是由它的类或显式类型声明决定。这个概念源自一句话:“如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子。” 2. 大鹅类型:由抽象类支持的方式,该方式会在运行时检查对象是否符合抽象基类要求。 3. 静态类型:C等传统静态类型语言采用的方式,即可以指定类型,但需要注意不管怎么说Python始终是动态类型的语言。 4. 静态鸭子类型:因Go语言而流行的方式,是一种在静态类型检查中模拟鸭子类型行为的方式。它允许在编译时(或静态分析时)检查对象是否具有所需的方法或属性,而不需要显式声明继承关系或接口。

防御性编程和“快速失败”

防御性,即有一套提高安全的实践,即使是粗心的程序员也不会造成灾难;而快速失败是指尽早地抛出运行时错误,例如,在函数主体开头就拒绝无效参数。

大鹅类型

Python并不像TS有interface关键字,在Python中我们使用抽象基类定义接口,在运行时显式检查类型。

大鹅类型要求: * 定义抽象基类的子类,明确你在实现既有接口 * 运行时检查类型时,isinstance和issubclass的第二个参数要使用抽象基类,而不是具体类

抽象基类是实现大鹅类型的工具,它允许你: * 通过抽象方法(@abstractmethod)明确声明一个类需要实现哪些方法 * 子类必须实现抽象基类中定义的所有抽象方法,否则无法实例化 * 通过继承抽象基类,明确表示一个类实现了某个接口

比如:

from abc import ABC, abstractmethod

# 定义抽象基类
class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

# 实现抽象基类
class Dog(Animal):
    def speak(self):
        return "Woof!"

# 运行时类型检查
def check_animal(animal):
    if isinstance(animal, Animal):
        print(animal.speak())
    else:
        raise TypeError("Expected an Animal")

dog = Dog()
check_animal(dog)  # 输出: Woof!

继承

这一章可以很好的用到其他主流面向对象语言的经验,Python是支持多重继承的,就和C++一样。

super函数

坚持使用内置函数super()是确保面向对象的Python程序可维护性的基本要求。

调用父类方法

在子类方法中,super().method() 允许调用父类的同名方法,而不是直接使用 ParentClass.method(self),这样可以避免硬编码父类名称,提高代码的可维护性。

class Parent:
    def show(self):
        print("Parent method")

class Child(Parent):
    def show(self):
        super().show()  # 调用父类的 show 方法
        print("Child method")

c = Child()
c.show()
支持多重继承(Method Resolution Order, MRO)

在多重继承时,super() 遵循方法解析顺序(MRO) 规则,可以确保正确调用多个父类的方法,而不会导致重复调用或错误。

class A:
    def show(self):
        print("A method")

class B(A):
    def show(self):
        print("B method")
        super().show()  # 按 MRO 规则调用下一个类的方法

class C(A):
    def show(self):
        print("C method")
        super().show()

class D(B, C):  # 多重继承,继承顺序为 D -> B -> C -> A
    def show(self):
        print("D method")
        super().show()

d = D()
d.show()

__init__方法中调用父类构造函数

在子类的 __init__ 方法中,super().__init__() 可确保父类的初始化逻辑执行,特别是在多重继承时很有用。

class Parent:
    def __init__(self):
        print("Parent init")

class Child(Parent):
    def __init__(self):
        super().__init__()  # 调用父类的构造方法
        print("Child init")

c = Child()

内置类型的继承

在Python早期版本中,内置类型不能子类化。虽然现在内置类型也可以子类化,但是有一个重要的注意事项:内置类型(使用C语言编写)通常不调用用户定义的类覆盖的方法。比如:

class DoppelDict(dict):
    def __setitem__(self,key,value):
        super().__setitem__(key,[value]*2)
dd = DoppelDict(one=1)
dd['two']=2
dd.update(three=3)
print(dd)
输出的结果是{'one': 1, 'two': [2, 2], 'three': 3},这个例子中1和3都没重复,说明继承自dict__init__update并没有使用我们自定义的__setitem__

这种行为叫晚期绑定,就和C++的虚函数是一样的,对于x.method()形式的调用,具体的方法必须在运行时根据接受者x所属的类确定。所以不应该直接继承内置类型(例如dict、list或者str),这很容易出错,应该从collection模块中的UserDict、UserList等继承,这些类是直接使用Python编写的

混入类

在 Python 中,混入类(Mixin) 是一种用于代码复用的设计模式,主要用于提供额外功能,而不是单独实例化。其实感觉像是为了更好的封装,把某些辅助模块给单独拆开了

class LogMixin:
    def log(self, message):
        print(f"[LOG] {message}")

class User(LogMixin):
    def __init__(self, name):
        self.name = name

    def say_hello(self):
        self.log(f"User {self.name} says hello!")  # 来自 LogMixin
        print(f"Hello, I am {self.name}!")

# 测试
u = User("Alice")
u.say_hello()

运算符重载

这一章还是可以参考C++,不过Python不能重载的运算符有:is,and,or和not

基本运算符重载

Python 通过定义类的特殊方法(魔术方法)来重载运算符,例如: * __add__(+) * __sub__(-) * __mul__(*) * __truediv__(/) * __mod__(%) * __pow__(**)

比如

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        raise TypeError("Unsupported operand type for +")

比较运算符

其实只需要重载__eq____lt__

索引和切片运算符

  • __getitem__(self, key):支持 obj[key]
  • __setitem__(self, key, value):支持 obj[key] = value
  • __delitem__(self, key):支持 del obj[key]

自定义对象转换

  • __int__() -> int(obj)
  • __float__() -> float(obj)
  • __str__() -> str(obj)
  • __repr__() -> repr(obj)

算术运算符的反向重载

对于 a + b,如果 a 没有 __add__,或者 a.__add__(b) 返回 NotImplemented,Python 会尝试 b.__radd__(a),以此类推