作者: 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" —— 相同内容获取相同条目

NotImplementedNotImplementedError 不同。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__:对象被垃圾回收前调用,不应依赖(不确定时机)。

十、本章思维导图

第11章 符合Python风格的对象

__repr__ / __str__ / __format__

控制字符串表示

__eq__与__hash__

若__eq__自定义,建议实现__hash__
前提是对象不可变

不可变对象 → 可散列

比较运算符

@total_ordering辅助

__lt__实现后自动补全

__slots__

节省内存

限制动态属性

@property

只读属性

验证/转换赋值

@classmethod / @staticmethod

工厂方法

同名函数组织

私有与保护

__attr → _ClassName__attr

_attr 约定

Vector 完整示例

整合上述所有特性

十一、常见错误与最佳实践

错误 原因 解决方案
忘记实现 __repr__ 默认的 __repr__ 只显示内存地址 始终实现 __repr__,至少返回 <ClassName field1, field2> 形式的字符串
在可变类上实现 __hash__ 可变对象作为字典键会导致灾难 确保对象不可变(例如所有属性只读)后实现 __hash__
误用 NotImplementedNotImplementedError 将异常当作返回值 明确:__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 一样方便地使用。


本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐