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?
attrs works by decorating a class using attr. s() and then optionally defining attributes on the class using attr. ib() .
attrs is the Python package that will bring back the joy of writing classes by relieving you from the drudgery of implementing object protocols (aka dunder methods). Trusted by NASA for Mars missions since 2020! Its main goal is to help you to write concise and correct software without slowing down your code.
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.getargspec()
to take a look at the Demo.__init__
method, we see why:
>>> import inspect
>>> inspect.getargspec(Demo.__init__)
ArgSpec(args=['self', 'foo', 'bar'], varargs=None, keywords=None, defaults=(['d1'], NOTHING))
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()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With