Blog

dataclass、普通类、NamedTuple 还是 dict?

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 先保持为字典,等有了理由再给它定义类型。

这篇文章对你有帮助吗?

点一颗爱心来评分!

平均评分 0 / 5. 投票总数: 0

还没有人投票。来做第一个评分的人吧。