Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When and why should I use attr.Factory?

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?

like image 441
jackalack Avatar asked May 23 '17 17:05

jackalack


People also ask

What does ATTR IB do?

attrs works by decorating a class using attr. s() and then optionally defining attributes on the class using attr. ib() .

What is ATTR S in Python?

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.


1 Answers

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()
like image 95
Martijn Pieters Avatar answered Sep 25 '22 16:09

Martijn Pieters