什么时候以及为什么应该使用 attr.Factory?

22

什么情况下我应该使用 attr.ib(default=attr.Factory(list)) 而不是 attr.ib(default=[])

文档中,我看到 Factory 用于生成新的值,如果你正在使用带有输入的 lambda 表达式,那么这很有意义;但是,如果你只是生成一个空列表,我不明白为什么要这样做。

我在这里错过了什么吗?


2
default=[] 生成一个空列表。一,大家都要分享。 - user2357112
1个回答

33

您希望避免使用可变对象作为默认值。如果您使用attr.ib(default=[]),则生成的是一个__init__方法,其中将该列表对象用作关键字参数默认值:

def __init__(self, foo=[]):
    self.foo = foo

参数的默认值是在定义时创建的,仅创建一次。每次调用方法时不会重新评估它们。对该对象的任何更改都将在所有实例之间共享。请参见"Least Astonishment" and the Mutable Default Argument

然而,使用attr.Factory()方法,则将默认值设置为哨兵值。当参数被留作哨兵值时,在函数内部则将其替换为调用工厂的结果。这相当于:

def __init__(self, foo=None):
    if foo is None:
        foo = []
    self.foo = foo

现在每个实例都创建了一个新的列表对象。

下面是一个快速演示,展示它们之间的差异:

>>> import attr
>>> @attr.s
... class Demo:
...     foo = attr.ib(default=[])
...     bar = attr.ib(default=attr.Factory(list))
...
>>> d1 = Demo()
>>> d1.foo, d1.bar
([], [])
>>> d1.foo.append('d1'), d1.bar.append('d1')
(None, None)
>>> d1.foo, d1.bar
(['d1'], ['d1'])
>>> d2 = Demo()
>>> d2.foo, d2.bar
(['d1'], [])

由于demo.foo使用了共享列表对象,通过d1.foo对它所做的更改会立即在任何其他实例下可见。

当我们使用inspect.getargspec()查看Demo.__init__方法时,就会明白原因:

>>> import inspect
>>> inspect.getargspec(Demo.__init__)
ArgSpec(args=['self', 'foo', 'bar'], varargs=None, keywords=None, defaults=(['d1'], NOTHING))
< p > foo 的默认值是完全相同的列表对象,附加了字符串 'd1'bar 被设置为一个标志对象(在这里使用 attr.NOTHING; 这个值使得可以使用 Demo(bar=None) 而不会将其转换成列表对象):

>>> print(inspect.getsource(Demo.__init__))
def __init__(self, foo=attr_dict['foo'].default, bar=NOTHING):
    self.foo = foo
    if bar is not NOTHING:
        self.bar = bar
    else:
        self.bar = __attr_factory_bar()

在Python 3.11中缺少inspect.getargspec,替代方案似乎是inspect.getfullargspecinspect.signature - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接