Messenger 在 Martin Fowler 的 Patterns of Enterprise Application Architecture 中被称为 Data Transfer Object, 它的作用是在程序里传递一包 information。 最常见的用途是携带 function 的多个 return value。 tuple 和 dictionary 也能完成这件事,但两者都依赖 indexing。 tuple 要求 consumer 自己记住 position 的数字顺序; dict 则需要更笨重的 d["name"] syntax。
Messenger 是一个 object,它的 attribute 与你要传递或返回的数据 name 一一对应:
# messenger_idiom.py
from typing import Any
class Messenger:
def __init__(self, **kwargs: Any) -> None:
self.__dict__ = kwargs
m: Any = Messenger(info="Spam", tags=["urgent", "todo"])
print(vars(m))
#: {'info': 'Spam', 'tags': ['urgent', 'todo']}
m.more = 11
print(m.info, m.tags, m.more)
#: Spam ['urgent', 'todo'] 11
print(vars(m))
#: {'info': 'Spam', 'tags': ['urgent', 'todo'], 'more': 11}
constructor 把 object 的 __dict__ 替换成 **kwargs parameter 自动创建的 dict。 vars(m) 返回的正是同一个 __dict__, 输出说明 attribute 和 keyword argument 实际上就是同一个 dict: m.more = 11 相当于增加一个 key, 就像 constructor 接收到 more=11 一样。
由于 **kwargs 是唯一 parameter, Messenger 只接受 keyword argument。 Messenger("Spam") 会抛 TypeError。 Positional-Only and Keyword-Only Parameters 中的 * marker 在这里不需要, 而 def __init__(self, *, **kwargs) 本身就是 syntax error, 因为 bare * 后面必须跟一个 named parameter。
m: Any annotation 不是装饰。 没有它,type checker 会同时拒绝 m.more = 11 和 m.info, 因为 Messenger class 没有声明任何 attribute。 Any 对 m 关闭了 type checker。 你也可以把这个 Any 移到 class 内部,而不是在每个 use site 重复写: 声明一个返回 Any 的 __getattr__(),以及一个接受 Any 的 __setattr__() (Surrogate 会解释 __getattr__() fallback hook)。 如果只声明前者,type checker 仍然会拒绝 write:m.more = 11。 standard library 为 SimpleNamespace 提供的 stub 声明了这样一对 method (read 那一半是 __getattribute__(),它会 intercept 每次 attribute access), 所以后面的 listing 无需 annotation。 这种 ad-hoc attribute bag 的代价,是没有 type checker 知道你有哪些 attribute name。 像 m.inof 这样的 typo 会在 runtime 得到 AttributeError,而不是 static error。
在标准 Python library 中, types.SimpleNamespace 就是现成的 Messenger。 同样,keyword argument 会变成 instance __dict__ 中的 attribute:
# display_namespace.py
from types import SimpleNamespace
m = SimpleNamespace(info="Spam", tags=["urgent", "todo"])
print(vars(m))
#: {'info': 'Spam', 'tags': ['urgent', 'todo']}
m.more = 11
print(m)
#: namespace(info='Spam', tags=['urgent', 'todo'], more=11)
print(m == SimpleNamespace(info="Spam",
tags=["urgent", "todo"],
more=11))
#: True
第一个 print() 展示了与手写版本相同的 instance __dict__。 SimpleNamespace 还补齐了其他功能:可读的 repr(),以及按内容比较的 equality。 Messenger 会打印成 <Messenger object at 0x...>, 两个 attribute 完全相同的 Messenger 也会比较为不相等, 因为它继承的是 object 基于 identity 的 equality。
SimpleNamespace 同样接受你任意发明的 name, 所以没有 type checker 能提前知道应该有哪些 name。 它的 type declaration 就是这么定义的:读取任意 attribute 都得到 Any, 因此这里的 m.inof 也不会被 type checker 报告。
如果希望 field 有名字并且受到检查,就明确声明它们。 @dataclass 会根据 declaration 生成 __init__()、__repr__() 与 equality, 并产生一个 mutable record:
# point_dataclass.py
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(1.0, 2.0)
print(p)
#: Point(x=1.0, y=2.0)
p.x = 3.5
print(p)
#: Point(x=3.5, y=2.0)
NamedTuple 以相同方式声明 field,但产生 immutable record。 typing.NamedTuple 是 Containers 中 namedtuple() 的 class form。 二者都会构造 tuple subclass,它的 position 同时有 name; 但 class form 还能为每个 field 声明 type, 所以 type checker 知道 Color.r 是 int,而 functional form 中则不知道。 因为底层仍然是 tuple, 每个 field 既能按 name 读取,也能按 position 读取:
# color_namedtuple.py
from typing import NamedTuple
class Color(NamedTuple):
r: int
g: int
b: int
red = Color(255, 0, 0)
print(red)
#: Color(r=255, g=0, b=0)
print(red.r, red[0])
#: 255 255
try:
red.r = 9 # type: ignore
except AttributeError as e:
print(e)
#: can't set attribute
print(red._replace(g=128))
#: Color(r=255, g=128, b=0)
print(red._asdict(), Color._fields)
#: {'r': 255, 'g': 0, 'b': 0} ('r', 'g', 'b')
打印 NamedTuple 得到与 data class 类似的可读输出。 裸 tuple 只会打印 (255, 0, 0),迫使你自己数 position。 给 field 赋值会抛 AttributeError,ty 也会报告错误。 attribute bag 什么都抓不到; 声明过的 field 则能抓到。 由于 field 不能 mutation,_replace() 会产生更新后的 copy。 The General Form of replace() 中的 copy.replace() 可以对任何 immutable record 做同样事情,包括 frozen data class。 immutability 还让 record hashable, 因此 Color 可以作为 dict key 或加入 set。
immutability guarantee 只到 field 本身,不会深入 field 指向的 object。 一个持有 list 的 NamedTuple 仍然允许那个 list 修改, 与 frozen=True 的 leak 相同。 这种 record 也无法 hash,不管 list 实际上有没有被 mutation, 因为 tuple 的 hash 会 hash 自己的 content。 真正 immutable 的 record 需要 immutable field。
_replace()、_asdict() 和 _fields 前面的 underscore 并不表示 private。 NamedTuple 这样标记自己的 member,是为了避免和你声明的 field name 冲突。 record 完全可以有一个叫 replace 或 fields 的 field。