Update (2020/02/22): pydantic 被认为是更好的方案。
attrs
(GitHub | 官网)是一个 Python 第三方库,被 Twisted 的作者 强烈推荐。它解决的是使用 Python 写 OOP 程序的存在的一个大问题。
问题
Python 的 OOP 实践起来很蛋疼。因为一些原因使得大量的 Python 程序都存在 God object,程序耦合严重难维护。而造成这个问题的原因在于写一个 Python 的类需要大量的 boilerplate。比如你想要实现一个带一些基础功能的类,需要写这么一坨又长又臭的代码:
from functools import total_ordering
@total_ordering
class Point3D(object):
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __repr__(self):
return (self.__class__.__name__ +
("(x={}, y={}, z={})".format(self.x, self.y, self.z)))
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return (self.x, self.y, self.z) == (other.x, other.y, other.z)
def __lt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return (self.x, self.y, self.z) < (other.x, other.y, other.z)
解决
attrs
就是来解决这个问题的。看一段示例代码:
>>> import attr
>>> @attr.s
... class C(object):
... x = attr.ib(default=42)
... y = attr.ib(default=attr.Factory(list))
...
... def hard_math(self, z):
... return self.x * self.y * z
>>> i = C(x=1, y=2)
>>> i
C(x=1, y=2)
>>> i.hard_math(3)
6
>>> i == C(1, 2)
True
>>> i != C(2, 1)
True
>>> attr.asdict(i)
{'y': 2, 'x': 1}
>>> C()
C(x=42, y=[])
>>> C2 = attr.make_class("C2", ["a", "b"])
>>> C2("foo", "bar")
C2(a='foo', b='bar')
写类从没有变得如此方便。
attrs
它只是智能地生成一堆 dundle (双下划线)函数以实现 repr
、相等比较等效果。它生成完之后,就跟你手写的类一样,没有额外的运行时损耗。它还提供了一些函数用来做参数校验等。
所以,在写相对复杂的程序时,尽可能地都用上 attrs
,提供了方便同时也没什么坏处。
Why not...
Plain object
写起来太麻烦,上文已经说过。
tuple
只能靠位置来找属性,不能靠名字。同时结构的改动可能引起大量代码改动:
# 如果 get_point 再返回一个 z 值,所有调用的代码都需要动
x, y = get_point()
dict
它不是固定 fields 的。这意味着可能有代码会增加新的元素或者删掉不应该删掉的元素。
namedtuple
- 属性是只读的,很不灵活
- 又可以当类用又可以当 tuple,很容易迷惑
其他
Twisted 的作者 Glyph Lefkowitz 说他写 Python 十几年了,Python 一直是他的最爱。看了一些 Twisted 上的文档,感觉它是个 Python 功底非常深厚的人。他的博客值得一看。