《流畅的Python》读书笔记12: 第三部分 类和协议 - 符合 Python 风格的对象
作者: andylin02
学习章节: 第 11 章 符合 Python 风格的对象
关键词: 对象表示|__repr__|__str__|__format__|__eq__|__hash__|私有属性|__slots__|@property|向量类
一、本章概述
《流畅的 Python》第 11 章(第一版第 9 章)聚焦于如何创建**“Pythonic”的对象**——即行为符合 Python 语言惯用风格的自定义类。Python 的对象模型通过特殊方法(__repr__、__eq__、__hash__ 等)让用户定义的类能够与语言的核心特性无缝集成。本章以构建一个完整的二维向量(Vector)类为主线,逐步展示如何让自定义类支持:
- 良好的对象字符串表示(
__repr__、__str__、__format__) - 完整的比较运算符(
__eq__及通过functools.total_ordering辅助) - 哈希化(
__hash__)使对象可作字典键 - 私有属性和保护属性的命名约定
@property特性实现只读属性__slots__优化内存- 类方法与静态方法的合理使用
本章不仅仅是一系列特殊方法的列举,更是一个完整的“从零构建 Pythonic 类”的实战指南。
本章主要内容结构:
- 对象表示方法(
__repr__、__str__、__format__) - 向量类的逐步实现
- 可散列向量(
__hash__与__eq__) - Python 中的私有属性与“受保护”属性
__slots__的威力与局限@property用于属性校验- 类方法 vs 静态方法
二、对象表示方法
在 Python 中,对象的字符串表示由三个特殊方法控制:
| 方法 | 触发时机 | 目标受众 | 备注 |
|---|---|---|---|
__repr__ |
repr(obj)、控制台直接输入对象 |
开发者/调试 | 应返回可用于重新创建对象的字符串(如 Vector(3, 4)) |
__str__ |
str(obj)、print(obj)、f-string 的 !s |
最终用户 | 未实现时回退到 __repr__ |
__format__ |
format(obj, spec)、f-string 的 !r/!s 后的格式说明符 |
定制格式化输出 | 支持 __format__ 可提供灵活的格式化 |
2.1 最简单的对象表示
class Vector:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
# !r 确保使用参数的 repr(),防止转义问题
return f'Vector({self.x!r}, {self.y!r})'
def __str__(self):
return f'({self.x}, {self.y})'
v = Vector(3, 4)
print(repr(v)) # Vector(3, 4)
print(str(v)) # (3, 4)
print(v) # (3, 4) —— print 调用 str
2.2 format 实现自定义格式化
class Vector:
# ... 省略 __init__ 和 __repr__ ...
def __format__(self, fmt_spec=''):
"""支持格式说明符,例如 '2.3f' 或 '.5e'"""
# 分别格式化 x 和 y 分量,用 fmt_spec 作为格式说明符
components = (format(c, fmt_spec) for c in (self.x, self.y))
return f'({", ".join(components)})'
# 使用示例
v = Vector(3.1415926, 2.71828)
print(format(v, '.2f')) # (3.14, 2.72)
print(f'{v:.3e}') # (3.142e+00, 2.718e+00)
三、完整的 Vector 类:逐步构建
3.1 第一版:基础构造与表示
from math import hypot
class Vector:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __repr__(self):
return f'Vector({self.x!r}, {self.y!r})'
def __abs__(self):
return hypot(self.x, self.y)
def __bool__(self):
# 向量为零时返回 False
return bool(abs(self)) # 或 return self.x != 0 or self.y != 0
def __add__(self, other):
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
v1 = Vector(2, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(3, 6)
print(v1 * 3) # Vector(6, 12)
print(abs(v1)) # 4.47213595499958
print(bool(Vector(0,0))) # False
3.2 第二版:支持比较与哈希
为了让 Vector 成为可散列的类型(可用作字典的键),需要实现 __eq__ 和 __hash__。根据 Python 数据模型,如果定义了 __eq__ 且类是可变的,通常不应实现 __hash__(因为对象哈希值应在生命周期内不变)。我们的 Vector 设计中 x, y 一旦创建不可变,因此可以安全地实现 __hash__。
class Vector:
# ... 保留之前的 __init__, __repr__, __abs__, __bool__, __add__, __mul__
def __eq__(self, other):
if not isinstance(other, Vector):
return NotImplemented # 让 Python 尝试 other 的 __eq__
return self.x == other.x and self.y == other.y
def __hash__(self):
# 对元组 (self.x, self.y) 的散列值进行异或混合
return hash((self.x, self.y))
# 测试
v1 = Vector(3, 4)
v2 = Vector(3, 4)
print(v1 == v2) # True
print(hash(v1) == hash(v2)) # True
d = {v1: "point"}
print(d[v2]) # "point" —— 相同内容获取相同条目
NotImplemented与NotImplementedError不同。NotImplemented是一个特殊单例值,表示某个方法(如__eq__)未定义如何处理给定类型,Python 会尝试反向操作(如交换操作数)。而NotImplementedError是一个异常,一般在抽象方法中抛出。
3.3 使用 functools.total_ordering 简化比较
如果类实现了 __eq__ 和一个其它比较方法(如 __lt__),total_ordering 装饰器会自动补全剩余的 __le__、__gt__、__ge__。
from functools import total_ordering
@total_ordering
class Vector:
# ... 已有 __eq__
def __lt__(self, other):
if not isinstance(other, Vector):
return NotImplemented
# 按模长比较
return abs(self) < abs(other)
# 现在支持所有比较运算符
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 > v2) # True(因为模长 5 > 2.236)
print(v1 <= v2) # False
四、私有属性与“保护”约定
4.1 名称修饰(Name Mangling)
Python 没有真正的私有属性,但提供了一种名称修饰机制,防止子类意外覆盖父类的属性。以双下划线开头(不以双下划线结尾)的属性名会被重命名为 _ClassName__attr。
class MyClass:
def __init__(self):
self.__secret = 42
def get_secret(self):
return self.__secret
obj = MyClass()
print(obj.get_secret()) # 42
# print(obj.__secret) # AttributeError
print(obj._MyClass__secret) # 42 —— 仍然可以访问,但知道的人会避免
名称修饰的主要目的是防止意外覆盖,而非实现严格的访问控制。通常推荐的约定是:单下划线
_attr表示“受保护的”内部属性(仅供内部使用),双下划线用于可能被子类覆盖的“私有”属性(名称修饰)。
4.2 在 Vector 中为属性添加“保护”
通常我们不应让用户可以随意修改向量的 x, y 属性,但为了简单起见,Vector 设计为不可变(实例化后 x, y 不变)。我们可以将 _x, _y 设为受保护,并用 @property 提供公开只读访问。
class Vector:
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
return self._x
@property
def y(self):
return self._y
# ... 其余特殊方法使用 self.x 和 self.y 访问
五、@property 特性:计算属性与校验
@property 装饰器可以将方法转变为属性,支持额外的逻辑(如校验、惰性计算)。
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError("宽度必须为正数")
self._width = value
@property
def area(self):
"""只读属性,没有 setter"""
return self._width * self._height
rect = Rectangle(10, 5)
print(rect.area) # 50
rect.width = 20
print(rect.area) # 100
# rect.area = 200 # AttributeError: can't set attribute
5.1 在 Vector 中使用 property(可选)
如果希望向量分量只读:
class Vector:
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
return self._x
@property
def y(self):
return self._y
# 若需要修改,可以添加 setter,但不推荐
六、slots 优化内存
默认情况下,每个实例使用 __dict__ 存储属性,这会占用大量内存(尤其在百万级实例时)。__slots__ 类属性可以限制实例属性,并为每个实例节省内存(不再使用 __dict__)。
class Vector:
__slots__ = ('_x', '_y')
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
return self._x
# ... 其他方法
v = Vector(3, 4)
# v.z = 5 # AttributeError: 'Vector' object has no attribute 'z'
slots 的代价:
- 无法动态添加实例属性
- 类不能使用
__dict__属性(除非手动加入'__dict__'到__slots__) - 多继承时需小心处理
- 某些内置功能(如
weakref)可能需要额外定义'__weakref__'
只在处理大量实例(如数百万)时考虑
__slots__,否则会过早优化并牺牲灵活性。
七、类方法与静态方法
7.1 @classmethod
@classmethod 的第一个参数是类本身(通常命名为 cls),常用于替代构造函数(工厂方法)。
class Vector:
# ... 常规方法
@classmethod
def from_polar(cls, magnitude, angle_rad):
"""从极坐标创建 Vector"""
x = magnitude * cos(angle_rad)
y = magnitude * sin(angle_rad)
return cls(x, y)
vec = Vector.from_polar(5, 0.785) # 角度 45°
7.2 @staticmethod
@staticmethod 不需要隐式传入类或实例,只是将函数归纳到类命名空间中。
class Vector:
@staticmethod
def dot(v1, v2):
return v1.x * v2.x + v1.y * v2.y
# 调用方式:Vector.dot(v1, v2)
大多数情况下,使用模块级函数即可代替静态方法。
@staticmethod与@classmethod仅用于类命名空间组织。
八、完整 Pythonic Vector 类示例
整合本章所有知识点,给出一个符合 Python 风格的 Vector 实现(不可变、支持格式化、可散列、完备比较、内存优化)。
from math import hypot, sin, cos
from functools import total_ordering
@total_ordering
class Vector:
__slots__ = ('_x', '_y')
def __init__(self, x: float, y: float) -> None:
self._x = float(x)
self._y = float(y)
# 属性访问
@property
def x(self) -> float:
return self._x
@property
def y(self) -> float:
return self._y
# 工厂方法(极坐标)
@classmethod
def from_polar(cls, magnitude: float, angle_rad: float):
return cls(magnitude * cos(angle_rad),
magnitude * sin(angle_rad))
# 字符串表示
def __repr__(self) -> str:
return f'{type(self).__name__}({self.x!r}, {self.y!r})'
def __str__(self) -> str:
return f'({self.x}, {self.y})'
def __format__(self, fmt_spec: str = '') -> str:
if fmt_spec.endswith('p'):
# 自定义 'p' 表示极坐标格式
fmt_spec = fmt_spec[:-1]
magnitude = abs(self)
angle = self.angle()
return f'<{magnitude:{fmt_spec}}, {angle:{fmt_spec}}>'
# 普通笛卡尔坐标
components = (format(c, fmt_spec) for c in (self.x, self.y))
return f'({", ".join(components)})'
# 数值运算
def __abs__(self) -> float:
return hypot(self.x, self.y)
def __bool__(self) -> bool:
return bool(abs(self))
def __add__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar: float):
if not isinstance(scalar, (int, float)):
return NotImplemented
return Vector(self.x * scalar, self.y * scalar)
__rmul__ = __mul__ # 标量乘法顺序交换
# 比较与哈希
def __eq__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return self.x == other.x and self.y == other.y
def __lt__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return abs(self) < abs(other)
def __hash__(self):
return hash((self.x, self.y))
# 其他辅助方法
def angle(self) -> float:
"""返回极坐标角度(弧度)"""
return atan2(self.y, self.x)
# 静态方法
@staticmethod
def dot(v1, v2):
return v1.x * v2.x + v1.y * v2.y
# 使用演示
if __name__ == '__main__':
v = Vector(3, 4)
print(repr(v)) # Vector(3.0, 4.0)
print(v) # (3.0, 4.0)
print(format(v, '.2f')) # (3.00, 4.00)
print(format(v, '.3fp')) # <5.000, 0.927>(极坐标)
print(Vector(0, 0)) # (0.0, 0.0)
print(bool(Vector(0, 0))) # False
v2 = Vector(1, 2)
print(v + v2) # Vector(4.0, 6.0)
print(v * 2) # Vector(6.0, 8.0)
print(2 * v) # Vector(6.0, 8.0) (__rmul__ 生效)
print(v == Vector(3, 4)) # True
print(hash(v) == hash(Vector(3, 4))) # True
print(Vector.dot(v, v2)) # 3*1+4*2=11
九、对象生命周期的特殊方法
除了上述方法,还可以实现 __new__(控制实例创建)和 __del__(终结器),但通常不需要:
__new__:在__init__之前调用,用于创建不可变类型或单例。__del__:对象被垃圾回收前调用,不应依赖(不确定时机)。
十、本章思维导图
十一、常见错误与最佳实践
| 错误 | 原因 | 解决方案 |
|---|---|---|
忘记实现 __repr__ |
默认的 __repr__ 只显示内存地址 |
始终实现 __repr__,至少返回 <ClassName field1, field2> 形式的字符串 |
在可变类上实现 __hash__ |
可变对象作为字典键会导致灾难 | 确保对象不可变(例如所有属性只读)后实现 __hash__ |
误用 NotImplemented 与 NotImplementedError |
将异常当作返回值 | 明确:__eq__ 无法处理对方类型时返回 NotImplemented;抽象方法中抛出 NotImplementedError |
滥用 __slots__ |
在不必要的地方使用导致代码僵化 | 仅在实例数量巨大(>10万)且内存瓶颈时使用 |
__format__ 未正确处理空格式说明符 |
format(obj) 应使用默认格式 |
在 __format__ 开始时检查 fmt_spec,为空时使用合理的默认格式 |
最佳实践清单:
- 为所有自定义类提供
__repr__(至少包含类名和关键状态) - 如果类用于存储“值”(类似值对象),实现
__eq__和__hash__,并确保类不可变 - 使用
@property代替公开的属性,以便后续添加验证逻辑 - 谨慎使用
__slots__,优先保持灵活性 - 需要替代构造函数时使用
@classmethod - 若无需访问实例或类,直接使用模块级函数而非
@staticmethod
十二、本章总结
通过本章的学习,你掌握了 Python 中创建一个“合格”对象所需的核心技能:
- 对象表示:
__repr__供调试,__str__供展示,__format__让对象适应格式语言。 - 行为定制:
__abs__、__bool__、__add__等让自定义类与内置类型无缝融合。 - 可散列性:实现
__eq__和__hash__,将对象变为可哈希的键。 - 内存优化:
__slots__在必要时大幅减少内存占用。 - 属性管理:
@property提供优雅的只读/校验接口。 - 类组织:
@classmethod与@staticmethod合理使用。
完成本章后,你应该能够设计出既符合 Python 习惯又高效的自定义类,为后续学习抽象基类、描述符和元编程夯实基础。
十三、下一章预告
第 12 章《序列的修改、散列和切片》
既然你已经知道如何构建一个“普通”的 Pythonic 类,那么下一章将深入到序列类型的世界,学习如何让自定义类具有列表或元组的行为。
第 12 章会以改进的 Vector 类(支持多维向量)为例,讲解:
- 实现序列协议(
__len__、__getitem__、__setitem__、__delitem__) - 让对象支持切片(正确处理
slice对象) - 实现动态属性访问
- 保持类的可散列性
- 使用
__slots__与序列特性的兼容
你将会看到,Python 的序列协议使得自定义序列类型既简单又强大,可以像内置 list 一样方便地使用。
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)