class-level attribute 的行为,常常会让从 C++ 或 Java 转来的程序员感到意外。
在 class body 内部、任何 method 外部声明的 field,是类属性(class attribute)。 它很容易被误认为“每个对象自己的默认值”。 但它不是。
一个 class attribute 会在这个 class 的所有 instance 之间创建一个共享变量。
如果之后又创建一个同名 instance attribute, 这个 instance attribute 会遮蔽(shadow) class attribute。 在 C++ 或 Java 中,语言会在 constructor 运行之前就为这种 field 在每个 object 内部分配存储, 所以 Python 的这种行为很容易令人意外。 Python class attribute 对应的是 C++ 或 Java 中的 static field。 Python 没有在 class body 中声明 per-object field 的语法。 应该在 method 内通过 self 赋值,从而创建那份 instance storage。
下面这个例子展示了为什么它很容易让人困惑:
# class_attribute_confusion.py
class Stars:
rating = 5 # Shared across all instances
a, b = Stars(), Stars()
print(a.rating, b.rating) # Both read the same storage
#: 5 5
a.rating = 1 # Assigning makes an instance attribute on 'a'
# 'a' shadows it, 'b' sees the class
print(a.rating, b.rating)
#: 1 5
Stars.rating = 9 # Change the shared storage
print(a.rating, b.rating) # 'b' reads the class attribute
#: 1 9
instance 和它的 class 各自都有自己的 attribute dictionary。 读取 attribute 时先检查 instance,然后回退到 class。 通过 instance 赋值则总是写入 instance, 第一次赋值时就会创建该 instance attribute。 通过 class 名称赋值,例如 Stars.rating = 9, 修改的是共享 value。 vars() 返回对象自身的 attribute dictionary, 因此分别用 vars(A) 检查 class、用 vars(a) 检查 instance,就能看到二者分离:
# inside_objects.py
class A:
x = 100 # Class attribute
a = A()
print(vars(A)["x"]) # The attribute lives in the class dict
#: 100
print(vars(a)) # The instance has no attributes yet
#: {}
a.x = 1
print(vars(a)) # Assignment created it on the instance
#: {'x': 1}
print(vars(A)["x"])
#: 100
代码清单对 vars(A) 做下标访问,是因为 class 的 dictionary 是只读 mappingproxy, 除了 x 之外还包含 compiler 自己的 bookkeeping 信息。 instance dictionary 则是普通 dict,只保存代码实际给它赋过的内容。
method 和其他任何 class attribute 一样。 在 class body 里写 def show(self):,会把一个 function object 存入 class dictionary, 而 a.show() 会通过和查找 a.x 相同的 fallback 找到它: instance 上没有,于是去 class 上查。 07 · 类中的 inspection helper display_object() 会把 attribute 和 method 分开报告, 但两者实际上都存放在同一个 class dictionary 中。 这就是为什么 a.show = something 会只针对 a 遮蔽这个 method。
有一种 class attribute 不遵循这种普通查找规则。 类中的 @property 会在 class 上占据自己的名称, 因此读取时调用 getter,赋值时调用 setter, 两者都不会碰 instance dictionary。 本章其余内容讨论的是 class body 中保存的普通 value。
class attribute 看起来一直很像默认值,直到某个人给某个 instance 的同名 attribute 赋值。 从那之后,修改 class attribute 会影响所有尚未 shadow 它的 object, 而已经 shadow 的那个 object 会继续保留自己的 value。 bug 往往会在离真正造成问题的代码很远的位置才暴露。
而且,只有当共享 value 是 immutable 时,shadowing 规则才能真正保护你:
# shared_mutable.py
class Cart:
items: list[str] = [] # One list, shared by every Cart
a, b = Cart(), Cart()
a.items.append("apple") # Mutates, does not assign
print(a.items, b.items)
#: ['apple'] ['apple']
a.items = ["pear"] # Assignment shadows, as before
print(a.items, b.items)
#: ['pear'] ['apple']
a.items.append("apple") 从来没有给 a.items 赋值。 它先读取 items,在 a 上找不到,于是回退到 class, 然后修改存放在 class 上的那一个共享 list。 mutation 不会创建 instance attribute,因此 b 也会看到 apple。 下一行则真的做了 assignment, 所以它会在 instance 上创建 a.items 并 shadow class list, 此后 b 仍然读取共享版本。 读取这一半没有任何保护:shadowing 是从 assignment 才开始的, 而 .append() 并没有赋值。 type checker 在这里也帮不上忙, 因为 a.items.append("apple") 对一个 list[str] 来说完全是合法调用。 本章末尾的真正的逐对象默认值 会让每个 object 都得到自己的 value。 可变默认值应该放进 @dataclass field,并使用 default_factory, 这一点会在把数据类作为类型中介绍。
当你确实需要一个共享 value 时,应通过 typing 中的 ClassVar 明确表达。 type checker 会把它视为 class-wide, 并拒绝通过 instance 直接赋值、从而意外 shadow 它: