When and why should I use attr.ib(default=attr.Factory(list))
over attr.ib(default=[])
?
From the docs I see that a Factory is used to generate a new value, which makes sense if you are using a lambda expression with inputs; however, I do not understand why you would use it if you are simply generating an empty list.
What am I missing here?
You want to avoid using mutable objects as defaults. If you used attr.ib(default=[])
, what is generated is an __init__
method using that list object as a keyword argument default:
def __init__(self, foo=[]):
self.foo = foo
Default values for arguments are created once, at definition time. They are not re-evaluated each time you call the method. Any mutations to that object are then shared across all instances. See "Least Astonishment" and the Mutable Default Argument.
Using the attr.Factory()
approach however, the default is set to a sentinel, and when the argument is left as the sentinel value, in the function itself is the value then replaced with the result of calling the factory. This is equivalent to:
def __init__(self, foo=None):
if foo is None:
foo = []
self.foo = foo
So now a new list object is created, per instance.
A quick demo demonstrating the difference:
>>> 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'], [])
Because demo.foo
is using a shared list object, changes made to it via d1.foo
are visible, immediately, under any other instance.
When we use inspect.signature()
to take a look at the Demo.__init__
method, we see why:
>>> import inspect
>>> inspect.signature(Demo)
<Signature (foo=['d1'], bar=NOTHING) -> None>
The default value for foo
is the very same list object, with the appended 'd1'
string still there. bar
is set to a sentinel object (here using attr.NOTHING
; a value that makes it possible to use Demo(bar=None)
without that turning into a list object):
>>> 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()