Когда и почему я должен использовать attr.Factory?
Когда и почему я должен использовать attr.ib(default=attr.Factory(list))
над attr.ib(default=[])
?
Из документации я вижу, что Factory используется для генерации нового значения, что имеет смысл, если вы используете лямбда-выражение с входными данными; однако я не понимаю, почему вы бы использовали его, если вы просто генерируете пустой список.
Что мне здесь не хватает?
1 ответ
Вы хотите избежать использования изменяемых объектов в качестве значений по умолчанию. Если вы использовали attr.ib(default=[])
, что генерируется __init__
метод с использованием этого объекта списка в качестве ключевого аргумента default:
def __init__(self, foo=[]):
self.foo = foo
Значения по умолчанию для аргументов создаются один раз, во время определения. Они не переоцениваются каждый раз, когда вы вызываете метод. Любые мутации этого объекта затем передаются всем экземплярам. См. "Наименьшее удивление" и Аргумент изменяемого по умолчанию.
С использованием 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))
Значение по умолчанию для 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()