2025, Sep 21 10:01
Значения по умолчанию в attrs без ошибок: @field.default и Factory(takes_self=True)
Показываем надёжные дефолты в attrs: @field.default и Factory(takes_self=True), порядок полей, как избежать NameError и AttributeError при инициализации
При использовании attrs для вычисления значений полей по умолчанию распространённая ловушка — сослаться на фабричную функцию до того, как она определена. Это приводит к NameError на этапе импорта и быстро выливается в путаницу с порядком определений, @field.default и Factory(takes_self=True). Давайте выделим ключевые элементы и покажем чистый, надёжный паттерн.
Минимальный рабочий паттерн
Лаконичный способ — привязать поставщик значения по умолчанию непосредственно к полю через декоратор .default этого поля. Функция оформляется как метод, получает self и может опираться на атрибуты, уже инициализированные в экземпляре.
from attrs import define, field
@define
class DataBox:
    label = field(init=False)
    @label.default
    def _label_maker(self):
        return "standard name"  # подставьте любую нужную вам логику вычисления
print(DataBox())
# DataBox(label='standard name')
Поскольку поставщик получает self, он может выводить значения из ранее инициализированных полей:
from attrs import define, field
@define
class DataBox:
    x = field()
    y = field()
    label = field(init=False)
    @label.default
    def _label_maker(self):
        return f"standard name {self.x * self.y}"
print(DataBox(x=7, y=6))
# DataBox(x=7, y=6, label='standard name 42')
Что именно идёт не так
Изначальный NameError возникает потому, что функцию передают в Factory до того, как её определили. В Python порядок определений важен. Если передать в Factory имя, которого ещё не существует, импорт модуля моментально провалится.
Порядок также важен для доступа к атрибутам внутри поставщика значения по умолчанию. Если поле пытается обратиться к self.some_attr, который ещё не установлен, при инициализации вы получите AttributeError.
from attrs import define, field
@define
class DataBox:
    label = field(init=False)
    x = field()
    y = field()
    @label.default
    def _label_maker(self):
        return f"standard name {self.x * self.y}"
print(DataBox(x=7, y=6))
# AttributeError: 'DataBox' object has no attribute 'x'
Здесь label объявлён раньше x и y, поэтому во время вычисления значения по умолчанию эти атрибуты ещё недоступны.
Решение с @field.default
Привяжите поставщик напрямую к полю и располагайте поля, от которых он зависит, выше. Метод получит self и сможет читать любые значения, уже выставленные в экземпляре. Такой подход компактный, понятный и исключает преждевременные ссылки на ещё не существующие имена.
from attrs import define, field
@define
class DataBox:
    x = field()
    y = field()
    label = field(init=False)
    @label.default
    def _label_maker(self):
        return f"standard name {self.x * self.y}"
Альтернатива через Factory(takes_self=True)
Если вам удобнее Factory, сначала определите метод‑поставщик, а затем используйте его в значении по умолчанию поля с параметром takes_self=True. Так вы избегаете NameError, гарантируя, что функция существует к моменту использования, и при этом сохраняете доступ к self.
from attrs import define, field, Factory
@define
class DataBox:
    def _label_maker(self):
        return "standard name"
    label = field(default=Factory(_label_maker, takes_self=True), init=False)
print(DataBox())
# DataBox(label='standard name')
И тот же приём, когда значение выводится из других атрибутов, при условии что эти поля объявлены раньше зависимого:
from attrs import define, field, Factory
@define
class DataBox:
    def _label_maker(self):
        return f"standard name {self.x * self.y}"
    x = field()
    y = field()
    label = field(default=Factory(_label_maker, takes_self=True), init=False)
print(DataBox(x=7, y=6))
# DataBox(x=7, y=6, label='standard name 42')
Если поменять порядок, зависимый метод ещё не увидит атрибуты, и вы получите AttributeError:
from attrs import define, field, Factory
@define
class DataBox:
    def _label_maker(self):
        return f"standard name {self.x * self.y}"
    label = field(default=Factory(_label_maker, takes_self=True), init=False)
    x = field()
    y = field()
print(DataBox(x=7, y=6))
# AttributeError: 'DataBox' object has no attribute 'x'
Почему это важно
В проектах, активно использующих attrs, значения по умолчанию нередко зависят от других полей. Понимание того, когда имена уже существуют, когда значения доступны через self и как порядок определений влияет на всё это, помогает избежать хрупкой инициализации. Плюс выигрыш в читаемости: будущему читателю сразу видно, какие поля участвуют в вычислении и как получается итоговое значение.
Итоги
Когда возможно, используйте декоратор .default у поля. Он явно фиксирует логику рядом с самим полем и предоставляет self, чтобы собирать значение из уже инициализированных атрибутов. Если предпочитаете Factory, сначала определите поставщик и передайте takes_self=True. В обоих вариантах держите зависимые поля выше того, которое от них зависит. Соблюдение порядка определений избавит от NameError при импорте и AttributeError при создании объекта.
Статья основана на вопросе на StackOverflow от Daemon Painter и ответе от simon.