Use attrs Library

22nd February 2020 at 4:57pm
Python: OOP

Update (2020/02/22): pydantic 被认为是更好的方案。

attrsGitHub | 官网)是一个 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 功底非常深厚的人。他的博客值得一看。