面向对象
约 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)
,以此类推