写 Python 的人大多用过类,但真正把面向对象写到位的并不多——__init__ 和 __repr__ 有什么区别?super() 在多重继承时到底怎么走?mixin 和普通父类该怎么选?SOLID 原则在动态语言里又该怎么落地?这些问题看似基础,却直接影响代码的可维护性和扩展性。
下面逐个拆解这些核心知识点,并给出可以直接跑的示例。
类与魔术方法:不止是 __init__
Python 的魔术方法(magic methods / dunder methods)是对象与语言基础设施的协议接口。__init__ 负责初始化实例状态,但对象的"构造"其实由 __new__ 完成;__repr__ 决定调试时看到什么,__str__ 决定用户看到什么。
几个容易忽略的细节:
__repr__应返回能重建对象的字符串(或尽量接近),__str__可以更友好。如果只实现一个,选__repr__——print()会 fallback 到它。__eq__和__hash__要一起考虑:实现了__eq__的对象默认__hash__为None,放进 set 或当 dict key 会直接报错。__len__、__getitem__让自定义容器无缝对接len()、索引和迭代。
class Vector:
"""一个简单的二维向量,演示核心魔术方法"""
__slots__ = ("_x", "_y") # 固定属性,省内存且防止误加属性
def __init__(self, x: float, y: float):
self._x = x
self._y = y
@property
def x(self) -> float:
return self._x
@property
def y(self) -> float:
return self._y
def __repr__(self) -> str:
return f"Vector({self._x}, {self._y})"
def __abs__(self) -> float:
return (self._x ** 2 + self._y ** 2) ** 0.5
def __eq__(self, other: object) -> bool:
if not isinstance(other, Vector):
return NotImplemented # 让 Python 尝试反方向比较
return self._x == other._x and self._y == other._y
def __hash__(self) -> int:
return hash((self._x, self._y))
def __add__(self, other: "Vector") -> "Vector":
return Vector(self._x + other._x, self._y + other._y)
# 运行验证
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(repr(v1)) # Vector(1, 2)
print(abs(v1)) # 2.23606797749979
print(v1 + v2) # Vector(4, 6)
print({v1, v2}) # {Vector(1, 2), Vector(3, 4)} — 可哈希
__eq__ 返回 NotImplemented 而不是 False,这是一个常见失误。返回 NotImplemented 时,Python 会尝试调用 other.__eq__(self),给对方一个机会;直接返回 False 则短路了这条路径。
继承与 Mixin:组合优于继承的落地方式
Python 的多重继承用 C3 线性化算法决定方法查找顺序(MRO),可以用 ClassName.__mro__ 查看。但多重继承容易变成"菱形问题"——两个父类都继承了同一个祖先,子类调用 super() 时到底走哪条路?
Mixin 是一种更安全的做法:Mixin 类只提供行为,不定义 __init__,不独立使用,也不参与菱形继承的核心路径。
class LogMixin:
"""为任何类添加日志能力——不定义 __init__,不引入状态"""
def log(self, message: str) -> None:
# 实际项目中替换为 logging 模块
print(f"[{self.__class__.__name__}] {message}")
class ValidateMixin:
"""为带 _data 字典的类添加字段验证"""
REQUIRED_KEYS: set[str] = set() # 子类覆盖
def validate(self) -> None:
missing = self.REQUIRED_KEYS - set(self._data.keys())
if missing:
self.log(f"缺少必要字段: {missing}")
raise ValueError(f"缺少: {missing}")
class UserConfig(LogMixin, ValidateMixin):
REQUIRED_KEYS = {"name", "email"}
def __init__(self, data: dict):
self._data = data
self.validate()
self.log("配置已加载")
def get(self, key: str, default=None):
return self._data.get(key, default)
# 正常使用
cfg = UserConfig({"name": "Alice", "email": "alice@example.com"})
print(cfg.get("name")) # Alice
# 缺字段时会报错并日志
try:
bad = UserConfig({"name": "Bob"})
except ValueError as e:
print(f"捕获异常: {e}")
Mixin 的关键约束:不要定义 __init__,不要假设子类的具体属性名(除非通过约定接口),只提供方法。这样 Mixin 之间不会争抢初始化顺序,MRO 也保持简单。
属性(Property):用方法伪装属性
@property 把方法调用伪装成属性访问,适合"读时计算"或"写时校验"的场景。但不要滥用——如果一个 getter 描次做大量计算或 I/O,它就不该是 property,因为使用者会以为 obj.x 是廉价操作。
class Temperature:
"""演示 property 的读写校验"""
def __init__(self, celsius: float):
self.celsius = celsius # 注意:这里走的是 setter
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError("温度不能低于绝对零度")
self._celsius = value
@property
def fahrenheit(self) -> float:
"""只读 property——换算结果,不存额外字段"""
return self._celsius * 9 / 5 + 32
# 不给 fahrenheit 写 setter → 只读
t = Temperature(25)
print(t.fahrenheit) # 77.0
t.celsius = 0
print(t.fahrenheit) # 32.0
try:
t.celsius = -300
except ValueError as e:
print(e) # 温度不能低于绝对零度
__init__ 里写 self.celsius = celsius 会走 setter,校验逻辑在构造阶段就生效——这是 property 的常见最佳实践。
SOLID 在 Python 中的务实解读
SOLID 原则源自静态类型语言,在 Python 里不需要死板照搬,但方向值得参考:
| 原则 | Python 下的要点 |
|---|---|
| S — 单一职责 | 一个类只变一个原因。如果类既有业务逻辑又管序列化,拆开。 |
| O — 开闭原则 | 用继承或 Protocol 扩展行为,不改已有代码。 |
| L — 里氏替换 | 子类能替父类用,不破坏约定。NotImplemented 而非 False 就是例子。 |
| I — 接口隔离 | 用 Protocol 定义小接口,别让实现类依赖用不到的方法。 |
| D — 依赖反转 | 高层模块依赖抽象(Protocol / ABC),不依赖具体实现。 |
下面用 Protocol 演示 D 和 I 的结合:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Shape(Protocol):
"""最小接口——只要能算面积就行"""
def area(self) -> float: ...
class Circle:
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def total_area(shapes: list[Shape]) -> float:
"""高层函数只依赖 Shape 协议,不关心具体类"""
return sum(s.area() for s in shapes)
# Circle 和 Rectangle 没继承任何基类,但满足协议
shapes = [Circle(1), Rectangle(2, 3)]
print(total_area(shapes)) # 9.14159
# runtime_checkable 允许 isinstance 检查
print(isinstance(Circle(1), Shape)) # True
Protocol 是 Python 3.8+ 的结构性子类型(duck typing 的类型检查版)。实现类不需要显式继承,只要方法签名匹配就满足协议——这比 ABC 更轻量,也更贴合 Python 风格。
自检清单
写完一个类之后,可以快速过一遍:
__repr__有没有实现?调试时你会感谢自己。__eq__/__hash__是否配套?需要放进 set/dict 的对象必须两者一致。- Mixin 是否避免了
__init__和实例状态?干净 Mixin 不抢初始化。 @property的 getter 是否是廉价操作?重计算用普通方法。- 子类 能否无缝替换父类使用?如果替换后行为异常,Liskov 被违反。
- 依赖 是否指向抽象(Protocol / ABC)而非具体类?高层模块不应因为换了一个实现就改代码。
这些点不需要每次全做,但遇到继承层次变深、类职责模糊时,对照检查能省下大量重构时间。面向对象不是写个 class 就完事——协议、约束和组合方式的选择,才是真正拉开差距的地方。