11 KiB
doc_type, full_text
| doc_type | full_text |
|---|---|
| short | sources/01_Dicts_revisited.md |
01_Dicts_revisited 总结
本文重新审视 Python 字典,说明 Python 的模块、对象、类、继承和方法调用机制在很大程度上都建立在字典之上。核心观点是:Python 对象系统可以理解为“字典之上的一层协议”。
核心主题
- 字典不仅是普通数据结构,也是 Python 解释器实现中的关键机制。
- 模块、实例和类都通过
__dict__保存名称到对象的映射。 - 属性访问
obj.name本质上会触发一套字典查找流程。 - 类共享方法和类变量,实例保存各自独立的数据。
- 继承通过
__bases__和__mro__扩展属性查找路径。 - 多重继承依赖 MRO 和 C3 线性化算法。
super()委托给 MRO 中的“下一个类”,是 mixin 模式的关键。
相关主题可整理为 Python对象模型、属性查找、继承与MRO、mixin模式。
字典与模块
Python 模块中的全局变量和函数都保存在模块字典中。
例如模块 foo.py:
x = 42
def bar():
...
def spam():
...
可以通过 foo.__dict__ 或 globals() 看到类似结构:
{
'x': 42,
'bar': <function bar>,
'spam': <function spam>
}
这说明模块命名空间本质上是一个字典。该思想与 Python命名空间 密切相关。
字典与对象实例
用户自定义对象的实例数据保存在实例自己的 __dict__ 中。
s = Stock('GOOG', 100, 490.1)
s.__dict__
结果类似:
{
'name': 'GOOG',
'shares': 100,
'price': 490.1
}
在构造函数中给 self 赋值,实际就是向实例字典写入键值对:
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
每个实例都有自己的独立字典:
s = Stock('GOOG', 100, 490.1)
t = Stock('AAPL', 50, 123.45)
因此,如果创建 100 个实例,就会有 100 个保存实例数据的字典。
类字典与共享成员
类本身也有一个字典,用于保存类定义中的方法和类变量。可通过 Stock.__dict__ 查看。
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
def sell(self, nshares):
self.shares -= nshares
类字典中会包含:
{
'__init__': <function>,
'cost': <function>,
'sell': <function>
}
实例数据位于实例字典,方法位于类字典。所有实例通过类共享这些方法。
实例与类的连接
每个实例都通过 __class__ 指向其所属类。
s.__class__
实例字典保存实例特有数据,类字典保存所有实例共享的数据和方法。
这一结构可以概括为:
s.__dict__:实例自己的属性。s.__class__:实例所属类。s.__class__.__dict__:类中定义的方法和类变量。
属性访问机制
对象属性访问使用点号操作:
x = obj.name # 读取
obj.name = value # 设置
del obj.name # 删除
这些操作都与底层字典相关。
设置与删除属性
设置属性会修改实例的 __dict__:
s.shares = 50
s.date = '6/7/2007'
此时实例字典可能变成:
{
'name': 'GOOG',
'shares': 50,
'price': 490.1,
'date': '6/7/2007'
}
删除属性也会从实例字典中移除键:
del s.shares
Python 默认不限制实例属性必须在 __init__() 中预先声明。也可以直接修改 __dict__:
goog.__dict__['time'] = '9:45am'
goog.time
不过直接操作 __dict__ 并不常见,正常代码应优先使用点号语法。
属性读取顺序
读取属性时,Python 会按顺序查找:
- 实例自己的
__dict__。 - 实例所属类的
__dict__。 - 如果涉及继承,则继续查找父类。
例如:
s.name
s.cost()
name 通常在实例字典中找到;cost 通常在类字典中找到。
这解释了为什么一个类中定义的方法可以被所有实例共享。该机制是 属性查找 的基础。
类变量与实例变量
在类体中直接赋值的变量是类变量,由所有实例共享:
class Foo:
a = 13
def __init__(self, b):
self.b = b
其中:
a是类变量,保存在Foo.__dict__中。b是实例变量,保存在各个实例的__dict__中。
示例:
f = Foo(10)
g = Foo(20)
f.a # 13
g.a # 13
f.b # 10
g.b # 20
如果修改类变量:
Foo.a = 42
所有未覆盖该属性的实例都会看到新值:
f.a # 42
g.a # 42
方法与绑定方法
调用实例方法其实涉及“绑定方法”机制。
s = goog.sell
此时 s 是一个 bound method,即绑定方法。它包含两部分:
s.__func__:真正实现该方法的函数对象。s.__self__:绑定到该方法的实例,即self。
因此:
s(25)
等价于:
s.__func__(s.__self__, 25)
这说明方法调用的本质是:从类字典中找到函数,再把实例作为第一个参数 self 传入。相关主题可归入 Python方法绑定。
继承的实现
类可以继承其他类:
class A(B, C):
...
父类保存在类的 __bases__ 属性中:
A.__bases__
继承会扩展属性查找路径:
- 先查找实例字典。
- 再查找当前类字典。
- 如果没有找到,沿父类继续查找。
单继承与 MRO
在单继承中,从子类到父类只有一条路径。Python 会沿继承链向上查找,遇到第一个匹配项就停止。
class A: pass
class B(A): pass
class C(A): pass
class D(B): pass
class E(D): pass
Python 会预先计算属性查找顺序,并保存在类的 __mro__ 中:
E.__mro__
结果类似:
(E, D, B, A, object)
MRO 即 Method Resolution Order,方法解析顺序。Python 按照 MRO 顺序查找属性,先找到者胜出。
多重继承与 C3 线性化
多重继承没有唯一的向上路径,因此属性查找顺序更复杂。
class A: pass
class B: pass
class C(A, B): pass
class D(B): pass
class E(C, D): pass
Python 使用协作式多重继承,并遵循两条直观规则:
- 子类总是在父类之前检查。
- 多个父类按声明顺序检查。
Python 会根据这些规则计算 MRO。例如:
E.__mro__
可能得到:
(E, C, A, D, B, object)
底层算法称为 C3 Linearization Algorithm。通常不需要掌握算法细节,但要记住:Python 通过 MRO 给复杂继承层次生成一个一致的线性查找顺序。
该部分是 继承与MRO 的核心内容。
Mixin 模式
文中通过 Dog、Bike、LoudDog、LoudBike 展示了多重继承的一个重要用途:mixin。
原始代码中,LoudDog.noise() 和 LoudBike.noise() 有相同逻辑:
return super().noise().upper()
可以把这段共同行为提取成一个 mixin 类:
class Loud:
def noise(self):
return super().noise().upper()
然后组合使用:
class LoudDog(Loud, Dog):
pass
class LoudBike(Loud, Bike):
pass
Loud 本身不能独立使用,它只是提供一个可混入的行为片段。通过多重继承,它可以给互不相关的类复用同一段功能。
这体现了 mixin模式 的典型用途:用小型类组合行为,而不是通过单一继承树表达所有关系。
为什么要使用 super()
覆盖方法时应使用 super():
class Loud:
def noise(self):
return super().noise().upper()
super() 并不简单表示“调用父类”,而是表示:调用 MRO 中的下一个类。
在多重继承中,你通常并不知道 MRO 中的下一个类具体是谁,因此硬编码某个父类方法会破坏协作式多重继承。super() 使多个类可以按照 MRO 顺序协同工作。
练习要点
Exercise 5.1:实例表示
通过查看 goog.__dict__ 和 ibm.__dict__,观察两个实例各自独立的数据字典。
Exercise 5.2:修改实例数据
给 goog 添加新属性:
goog.date = '6/11/2007'
只会影响 goog.__dict__,不会影响 ibm.__dict__。这说明实例属性是逐实例保存的。
也可以直接修改实例字典:
goog.__dict__['time'] = '9:45am'
随后可通过 goog.time 访问。
Exercise 5.3:类的作用
方法 cost 不在实例字典中,而在类字典中:
Stock.__dict__['cost']
可以直接通过类字典中的函数调用:
Stock.__dict__['cost'](goog)
这展示了 self 参数的真实传递方式。
添加类属性:
Stock.foo = 42
所有实例都能访问:
goog.foo
ibm.foo
但 foo 不在实例字典中,而是在类字典中。
Exercise 5.4:绑定方法
将方法取出:
s = goog.sell
得到的是绑定方法,包含函数和实例。调用:
s(25)
等价于:
s.__func__(s.__self__, 25)
Exercise 5.5:继承
定义子类:
class NewStock(Stock):
def yow(self):
print('Yow!')
实例 n 可以调用继承自 Stock 的 cost(),也可以调用自身定义的 yow()。
通过以下属性观察继承结构:
NewStock.__bases__
NewStock.__mro__
查找 cost() 时,Python 会沿 n.__class__.__mro__ 顺序查找各类的 __dict__,直到找到 cost。
关键结论
本文把 Python 对象系统拆解为几个核心机制:
- 模块是字典。
- 实例是带有
__dict__的对象。 - 类也是带有
__dict__的对象。 - 方法是类字典中的函数,通过绑定方法机制接收实例作为
self。 - 属性访问由实例字典、类字典和 MRO 共同决定。
- 继承是属性查找路径的扩展。
- 多重继承依赖 MRO 和
super()实现协作。 - Mixin 是多重继承在 Python 中最常见、最实用的模式之一。
整体而言,理解 __dict__、__class__、__bases__、__mro__ 和 super(),就能理解 Python 类与对象机制的大部分行为。