Python 有四种存放结构化数据的方式。选哪种不是风格问题,关键在于:拼错一个名字时,你希望程序立刻报错吗?
要存放几个相关的值,Python 给了你四种合理的方式。选错很少致命,却总让人有点烦,所以值得花十分钟一次搞清楚。
四种方式
d = {'x': 3, 'y': 4}
print(d, d['x'])
print('typo is silent:', d.get('z'))
try:
d['z']
except KeyError as err:
print('KeyError:', err)
{'x': 3, 'y': 4} 3
typo is silent: None
KeyError: 'z'
class P1:
def __init__(self, x, y): self.x, self.y = x, y
print(P1(3,4).x)
@dataclass
class P2:
x: int
y: int
print(P2(3,4), P2(3,4) == P2(3,4))
class P3(NamedTuple):
x: int
y: int
p3 = P3(3,4)
print(p3, p3.x, tuple(p3), p3 == (3,4))
3
P2(x=3, y=4) True
P3(x=3, y=4) 3 (3, 4) True
数据类(dataclass)根据类型注解帮你生成 __init__、__repr__ 和 __eq__。NamedTuple 除了这些,还提供不可变性、元组解包,以及和普通元组的相等比较。
可变性
p2 = P2(3,4); p2.x = 99
print('dataclass mutable:', p2)
try:
p3.x = 99
except AttributeError as err:
print('NamedTuple immutable:', type(err).__name__ + ':', err)
@dataclass(frozen=True)
class P4:
x: int
y: int
print('frozen dataclass hashable:', hash(P4(3,4)) == hash(P4(3,4)))
dataclass mutable: P2(x=99, y=4)
NamedTuple immutable: AttributeError: can't set attribute
frozen dataclass hashable: True
加上 frozen=True,数据类就有了 NamedTuple 的保证,顺带还变得可哈希。
决定性的问题:拼写错误会怎样
日常开发中,真正要紧的就是这一点。
p = P2(3,4)
p.z = 5 # a plain/dataclass object accepts anything
print('dataclass accepts a typo:', p.z)
try:
P3(3,4).z
except AttributeError as err:
print('NamedTuple rejects it:', type(err).__name__ + ':', err)
dataclass accepts a typo: 5
NamedTuple rejects it: 'P3' object has no attribute 'z'
字典的 .get() 会悄无声息地返回 None。普通类或数据类会悄无声息地接受一个你从没声明过的新属性:p.z = 5 不算错误,于是一次拼错的赋值就造出了一个没人会读的字段。NamedTuple 两种情况都会拒绝。
对于要到处传递的数据,这是选 NamedTuple 或冻结数据类最有力的理由:拼写错误在你犯错的地方就报出来,而不是三个函数之后。第 10 篇会讲 __slots__ 如何给可变类同样的保护。
内存
print('dict ', sys.getsizeof({'x':3,'y':4}))
print('NamedTuple ', sys.getsizeof(P3(3,4)))
print('class ', sys.getsizeof(P1(3,4)) + sys.getsizeof(P1(3,4).__dict__))
print('dataclass ', sys.getsizeof(P2(3,4)) + sys.getsizeof(P2(3,4).__dict__))
dict 184
NamedTuple 56
class 328
dataclass 304
NamedTuple 就是带名字的元组,所以每个实例都没有 __dict__。类和数据类有,而且它占了大部分体积。
这些数字只看大致规律,别当基准测试。它们随 Python 版本和存储内容而变。规律是稳定的:NamedTuple 小得多,字典居中,带 __dict__ 的对象最大。只有实例数到了几十万,这才有影响。
怎么选
dict:数据结构确实是动态的,来自 JSON,或者你事先不知道有哪些键。别再出于习惯把外部 JSON 转成对象了。
NamedTuple:要传来传去、拿来比较的小型不可变值。开销最小,最能防拼写错误,还支持元组解包。
dataclass:你自己定义的、字段超过三个左右的数据,或者构造后还需要修改的数据,默认就用它。除非有理由不加,否则加上 frozen=True。
普通类:行为比数据更重要的时候,比如方法、__init__ 里的校验、计算属性。第 1 篇的银行账户就是这种情况,那是另一回事。
要点
- 字典拼错键不会报错;类或数据类拼错属性名,会悄悄多出一个字段。
- NamedTuple 和
frozen=True的数据类两种错误都会拒绝。 - 默认用数据类,除非有东西需要修改,否则加上
frozen=True。 - 外部 JSON 先保持为字典,等有了理由再给它定义类型。