attrs: современные классы в Python без лишнего кода

02 октября 2026
Автор

Антон Ларичев

Что такое attrs

attrs — сторонняя библиотека Python, которая позволяет описывать классы декларативно: вы перечисляете атрибуты и их свойства, а библиотека сама генерирует __init__, __repr__, __eq__, __hash__ и другие методы. Это экономит сотни строк шаблонного кода и уменьшает вероятность ошибок.

Библиотека появилась в 2015 году и послужила вдохновением для стандартных dataclasses, добавленных в Python 3.7. Однако attrs предлагает значительно больше возможностей: встроенную валидацию, конвертеры, слоты, заморозку объектов и тонкую настройку поведения.

Курс по теме

Основы Python — курс

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Установка

pip install attrs

Библиотека поставляется в двух интерфейсах:

  • attrs — современный, рекомендуемый (модуль attrs)
  • attr — устаревший, обратно совместимый (модуль attr)

В новом коде используйте attrs.

Базовый пример

Класс без attrs

Посмотрим на типичный класс данных без библиотек:

class User:
    def __init__(self, name: str, age: int, email: str):
        self.name = name
        self.age = age
        self.email = email

    def __repr__(self):
        return f"User(name={self.name!r}, age={self.age!r}, email={self.email!r})"

    def __eq__(self, other):
        if not isinstance(other, User):
            return NotImplemented
        return (self.name, self.age, self.email) == (other.name, other.age, other.email)

Это 15 строк и ни строчки логики — только шаблон.

Тот же класс с attrs

import attrs

@attrs.define
class User:
    name: str
    age: int
    email: str

Три строки тела класса заменяют весь шаблон. @attrs.define автоматически генерирует __init__, __repr__, __eq__ и настраивает слоты (__slots__).

user = User(name="Alice", age=30, email="alice@example.com")
print(user)  # User(name='Alice', age=30, email='alice@example.com')

user2 = User(name="Alice", age=30, email="alice@example.com")
print(user == user2)  # True

Декораторы: @attrs.define vs @attr.s

Существует два поколения декораторов:

Декоратор Поколение Рекомендуется
@attrs.define Новое (attrs 20.1+) Да
@attrs.mutable Псевдоним для @attrs.define Да
@attrs.frozen Неизменяемый класс Да
@attr.s Старое Нет
@attr.attrs Псевдоним @attr.s Нет

В новом коде всегда используйте @attrs.define или @attrs.frozen.

Описание полей с attrs.field

Чтобы задать дополнительные параметры для атрибута, используйте attrs.field():

import attrs

@attrs.define
class Product:
    name: str
    price: float
    quantity: int = attrs.field(default=0)
    tags: list[str] = attrs.field(factory=list)
    _internal_id: int = attrs.field(init=False, default=0)

Основные параметры field()

import attrs

@attrs.define
class Config:
    host: str = attrs.field(default="localhost")
    port: int = attrs.field(default=8080)
    timeout: float = attrs.field(default=30.0)
    tags: list = attrs.field(factory=list)       # изменяемый default
    options: dict = attrs.field(factory=dict)    # изменяемый default
    debug: bool = attrs.field(default=False, repr=False)  # скрыть из repr
    _secret: str = attrs.field(default="", alias="secret")  # alias для __init__

Важно: для изменяемых значений по умолчанию (списки, словари) всегда используйте factory, а не default. Это предотвращает классическую ошибку с общим изменяемым объектом.

Валидаторы

Валидаторы проверяют значения при создании объекта и при изменении (если класс изменяемый).

Встроенные валидаторы

import attrs

@attrs.define
class User:
    name: str = attrs.field(
        validator=attrs.validators.instance_of(str)
    )
    age: int = attrs.field(
        validator=[
            attrs.validators.instance_of(int),
            attrs.validators.ge(0),    # greater or equal
            attrs.validators.le(150),  # less or equal
        ]
    )
    email: str = attrs.field(
        validator=attrs.validators.matches_re(
            r"^[\w.-]+@[\w.-]+\.\w+$"
        )
    )

Перечень встроенных валидаторов:

  • instance_of(type) — проверка типа
  • in_(collection) — значение входит в набор
  • not_(validator) — инверсия
  • and_(*validators) — все условия
  • or_(*validators) — хотя бы одно условие
  • ge(val), gt(val), le(val), lt(val) — числовые сравнения
  • max_len(n), min_len(n) — длина последовательности
  • matches_re(pattern) — регулярное выражение
  • deep_iterable(...) — рекурсивная проверка элементов
  • deep_mapping(...) — рекурсивная проверка словаря
  • is_callable() — значение вызываемо
  • optional(validator) — разрешает None

Пользовательский валидатор

import attrs

def positive_price(instance, attribute, value):
    if value <= 0:
        raise ValueError(
            f"Цена должна быть положительной, получено: {value}"
        )

@attrs.define
class Product:
    name: str
    price: float = attrs.field(validator=positive_price)

try:
    p = Product(name="Widget", price=-10.0)
except ValueError as e:
    print(e)  # Цена должна быть положительной, получено: -10.0

Валидатор получает три аргумента: экземпляр класса (instance), объект атрибута (attribute) и проверяемое значение (value).

Можно также использовать декоратор:

import attrs

@attrs.define
class BankAccount:
    balance: float = attrs.field(default=0.0)

    @balance.validator
    def check_balance(self, attribute, value):
        if value < 0:
            raise ValueError("Баланс не может быть отрицательным")

Конвертеры

Конвертеры автоматически преобразуют входные значения перед сохранением:

import attrs

@attrs.define
class Person:
    name: str = attrs.field(converter=str.strip)
    age: int = attrs.field(converter=int)
    tags: list[str] = attrs.field(
        converter=lambda v: [t.lower() for t in v]
    )

p = Person(name="  Alice  ", age="25", tags=["Python", "DJANGO"])
print(p.name)   # 'Alice'
print(p.age)    # 25 (int, не строка)
print(p.tags)   # ['python', 'django']

Конвертеры выполняются до валидаторов, поэтому тип будет уже нужным к моменту проверки.

Заморозка объектов: @attrs.frozen

Для создания неизменяемых объектов используйте @attrs.frozen. Попытка изменить атрибут вызовет FrozenInstanceError:

import attrs

@attrs.frozen
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
try:
    p.x = 3.0
except attrs.exceptions.FrozenInstanceError:
    print("Нельзя изменить замороженный объект")

Для создания модифицированной копии используйте attrs.evolve:

p2 = attrs.evolve(p, x=5.0)
print(p2)  # Point(x=5.0, y=2.0)
print(p)   # Point(x=1.0, y=2.0)  — оригинал не изменён

Слоты и производительность

@attrs.define по умолчанию использует __slots__, что даёт:

  • Меньший расход памяти (нет словаря __dict__)
  • Более быстрый доступ к атрибутам
  • Защиту от опечаток: нельзя присвоить несуществующий атрибут
import attrs

@attrs.define
class Vector:
    x: float
    y: float

v = Vector(1.0, 2.0)
try:
    v.z = 3.0  # AttributeError: 'Vector' object has no attribute 'z'
except AttributeError as e:
    print(e)

Если слоты мешают (например, при использовании __dict__ или динамических атрибутов), отключите их:

@attrs.define(slots=False)
class FlexibleClass:
    value: int

Хук attrspostinit

Для пост-инициализационной логики определите метод __attrs_post_init__:

import attrs
import math

@attrs.define
class Circle:
    radius: float
    area: float = attrs.field(init=False)
    circumference: float = attrs.field(init=False)

    def __attrs_post_init__(self):
        self.area = math.pi * self.radius ** 2
        self.circumference = 2 * math.pi * self.radius

c = Circle(radius=5.0)
print(f"Площадь: {c.area:.2f}")        # Площадь: 78.54
print(f"Длина окружности: {c.circumference:.2f}")  # Длина окружности: 31.42

Сериализация: asdict и astuple

import attrs

@attrs.define
class Address:
    street: str
    city: str
    country: str

@attrs.define
class User:
    name: str
    age: int
    address: Address

user = User(
    name="Alice",
    age=30,
    address=Address("Ленина 1", "Москва", "Россия")
)

# Конвертация в словарь (рекурсивная)
data = attrs.asdict(user)
print(data)
# {
#   'name': 'Alice',
#   'age': 30,
#   'address': {'street': 'Ленина 1', 'city': 'Москва', 'country': 'Россия'}
# }

# Конвертация в кортеж (рекурсивная)
tuple_data = attrs.astuple(user)
print(tuple_data)  # ('Alice', 30, ('Ленина 1', 'Москва', 'Россия'))

asdict удобно использовать для сериализации в JSON:

import json

json_str = json.dumps(attrs.asdict(user), ensure_ascii=False)
print(json_str)

Интроспекция классов

import attrs

@attrs.define
class Config:
    host: str = "localhost"
    port: int = 8080
    debug: bool = False

# Получить список полей
fields = attrs.fields(Config)
for field in fields:
    print(f"{field.name}: {field.type} = {field.default}")

# Проверить, является ли класс attrs-классом
print(attrs.has(Config))   # True
print(attrs.has(dict))     # False

Наследование

import attrs

@attrs.define
class Animal:
    name: str
    sound: str

@attrs.define
class Dog(Animal):
    breed: str
    sound: str = "Woof"  # переопределение default

d = Dog(name="Rex", breed="Labrador")
print(d)  # Dog(name='Rex', sound='Woof', breed='Labrador')

При наследовании поля родительского класса идут первыми. Если родительское поле имеет default, а дочернее нет, возникнет TypeError. Чтобы избежать этого, либо задайте default для всех дочерних полей, либо используйте kw_only=True:

@attrs.define
class ExtendedDog(Dog):
    age: int = attrs.field(kw_only=True)

d = ExtendedDog(name="Rex", breed="Labrador", age=3)

Сравнение с dataclasses

Возможность attrs dataclasses
Генерация __init__, __repr__, __eq__ Да Да
Встроенные валидаторы Да Нет
Конвертеры Да Нет
__slots__ по умолчанию Да Нет
attrs.evolve Да dataclasses.replace
attrs.asdict Да dataclasses.asdict
Поддержка Python 2 Нет (с 21.3) Нет (Python 3.7+)
Зависимость Сторонняя Стандартная библиотека
Производительность Выше Ниже

Выбирайте dataclasses если хотите нулевых зависимостей. Выбирайте attrs если нужны валидация, конвертеры или максимальная производительность.

Практический пример: модель заказа

import attrs
from datetime import datetime
from enum import Enum

class OrderStatus(Enum):
    PENDING = "pending"
    PAID = "paid"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

@attrs.define
class OrderItem:
    product_id: str
    quantity: int = attrs.field(
        validator=attrs.validators.and_(
            attrs.validators.instance_of(int),
            attrs.validators.gt(0)
        )
    )
    unit_price: float = attrs.field(
        validator=attrs.validators.gt(0)
    )
    total: float = attrs.field(init=False)

    def __attrs_post_init__(self):
        self.total = self.quantity * self.unit_price

@attrs.define
class Order:
    order_id: str
    customer_email: str = attrs.field(
        validator=attrs.validators.matches_re(
            r"^[\w.-]+@[\w.-]+\.\w+$"
        )
    )
    items: list[OrderItem] = attrs.field(factory=list)
    status: OrderStatus = attrs.field(default=OrderStatus.PENDING)
    created_at: datetime = attrs.field(factory=datetime.utcnow)
    total: float = attrs.field(init=False, default=0.0)

    def __attrs_post_init__(self):
        self.total = sum(item.total for item in self.items)

    def add_item(self, item: OrderItem) -> "Order":
        new_items = self.items + [item]
        new_total = sum(i.total for i in new_items)
        return attrs.evolve(self, items=new_items, total=new_total)

# Использование
order = Order(
    order_id="ORD-001",
    customer_email="alice@example.com",
    items=[
        OrderItem(product_id="P1", quantity=2, unit_price=499.99),
        OrderItem(product_id="P2", quantity=1, unit_price=1299.00),
    ]
)

print(f"Заказ {order.order_id}: {order.total:.2f} руб.")
# Заказ ORD-001: 2298.98 руб.

# Добавить товар без мутации
updated_order = order.add_item(
    OrderItem(product_id="P3", quantity=3, unit_price=99.50)
)
print(f"Обновлённый заказ: {updated_order.total:.2f} руб.")
# Обновлённый заказ: 2597.48 руб.

# Сериализация
import json
order_dict = attrs.asdict(order)
order_dict["status"] = order.status.value
order_dict["created_at"] = order.created_at.isoformat()

Итог

attrs решает одну задачу исключительно хорошо — описание классов данных без шаблонного кода. Ключевые преимущества:

  • Декларативный стиль уменьшает ошибки и улучшает читаемость
  • Встроенные валидаторы защищают от некорректных данных прямо при создании объектов
  • Конвертеры автоматизируют нормализацию входных данных
  • __slots__ по умолчанию снижает потребление памяти
  • attrs.evolve позволяет безопасно создавать изменённые копии

Библиотека хорошо подходит для доменных моделей, конфигурационных объектов, DTO и Value Objects в архитектурах DDD.

Подробнее о проектировании классов и ООП в Python вы можете узнать на курсе PurpleSchool: https://purpleschool.ru/course/python?utm_source=knowledgebase&utm_medium=text&utm_campaign=python-attrs

Стрелочка влевоPython dataclasses: создание классов данных

Постройте личный план изучения Python до уровня Middle — бесплатно!

Python — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Все гайды по Python

Как отправлять запросы с помощью requests в PythonПочему Python выводит значение без команды printКак работает команда print в PythonВозможности Python для автоматизации задачРабота с JSON в Python на примерахPython get — методы получения данныхКак находить и исправлять ошибки в PythonРабота с данными через API и внешние сервисыСтруктура и оформление кода PythonОсновы Django с PythonПолезные приёмы в Python для повседневной работыИспользование locals в Python для отладкиИнтеграция PHP и PythonКак выполнять HTTPS-запросы в PythonFastAPI Python — быстрый старт: создание REST API с нуляКак работать с API в Python
Ввод целого числа в PythonВедение логов в PythonУдаление данных в Python с помощью removeОбработка исключений с помощью try/except в PythonФункция super() в Python — как вызвать метод родителяСоздание собственных контекстных менеджеров в PythonРабота с символами программирования PythonРабота с переменной X в PythonРабота с классами в PythonКак скачать Python на компьютерПростая программа на Python для начинающихОсновы Python для тех, кто начинаетЧто нового в Python 3Поддерживается ли Python 2 и стоит ли его использоватьPython 1 — с чего начиналась история языкаКоманда python print - полное руководство по выводу данныхПравила именования переменных в PythonПользовательские исключения в PythonОсновы Python coreОписание объектов PythonНаследование классов в Python — основы и примерыМножественное наследование в Python — примеры и MROКонтекстный менеджер with в Python — как работает и зачем нуженКомментарии в Python — однострочные, многострочные и docstringКакой Python выбрать для установкиКак вывести целое число с помощью print в PythonКак установить Python на Windows macOS и LinuxКак пользоваться консолью PythonКак получить последний элемент в PythonКак найти значение в PythonКак настроить PythonКак использовать print для строк в PythonКак работает интерпретатор PythonИнструкция по работе с PythonБлок finally в обработке исключений PythonЦелые числа в PythonАбстрактные классы в Python — ABC и abstractmethod
Загрузка данных PythonУправление проектами на GitHub с PythonСоздание веб-приложений на Flask PythonСоздание бота на PythonСоздание интерфейсов Python QTСоздание игр с PygameСоздание GUI в PythonКак работать со словарями в PythonРабота с библиотеками через Python PackagingРабота со временем в Python при помощи модуля timePython name — особенности переменнойPython listing — что это и как использоватьМатематические операции в Python с модулем mathPytest — тестирование на Python: полное руководствоОбработка изображений с OpenCV PythonNumPy в Python — основы и применение в задачахМашинное обучение с PythonИспользование Anaconda с PythonМодуль contextlib в Python — утилиты для контекстных менеджеровБиблиотеки Python и их применение в проектах
Возврат значений из функции в PythonВложенные функции в PythonСоздание собственных декораторов в PythonРабота с функцией map в PythonЦикл while в Python и примеры использованияОбработка чисел, введённых через input в PythonОсновные операторы в Python с примерамиУсловные выражения if else в Python для начинающихКак выполняется вызов функций call в PythonПродвинутые генераторы в Python — send, throw, close и корутиныПозиционные и именованные аргументы в PythonОбъявление переменных и управление областью видимости в PythonПередача аргументов по ссылке и по значению в PythonПередача аргументов через args и kwargs в PythonОсновные методы Python и примеры их использованияОператор match/case в Python 3.10+ — основы структурного сопоставленияПаттерны match/case в Python — деструктуризация, guard и вложенные шаблоныПрактические примеры match/case в Python — реальные сценарии примененияЛокальные и глобальные переменные в PythonЧасто используемые команды PythonКлючевые слова global и nonlocal в PythonКак создавать функции в PythonКак работает сборщик мусора в PythonКак работает область видимости переменных в PythonКак работает функция callable в PythonКак работает функция any и all в PythonКак проверить тип переменной в PythonКак передать функцию как аргумент в PythonКак использовать функцию isinstance в PythonКак использовать функцию filter в PythonКак использовать функцию filter в PythonКак использовать функцию eval безопасно в PythonКак использовать декораторы в PythonГенераторы и yield в Python — как создавать и использоватьИзменяемые и неизменяемые типы данных в PythonГенераторные выражения в Python — синтаксис и примерыФункции в Python и способы их вызоваФункции как объекты в PythonЧто такое замыкания в PythonЧто делает функция reduce в PythonЧто делает функция id в PythonАргументы по умолчанию в PythonАнонимные функции и lambda в PythonАлгоритмы на Python — примеры и объяснение
Установка pip в PythonЗапись данных в PythonУправление зависимостями requirement в PythonУправление библиотеками с помощью Python PackagingУдаление пробелов с помощью strip в PythonСоздание исполняемого файла Python в exeСтруктурирование кода в PythonРазбор traceback в модуле PythonРазбор site-packages в PythonРазбор Program Files в PythonРабота с Unicode кодировками в PythonРабота с системными функциями Python sysРабота с папкой AppData в PythonРабота с модулем logging в PythonРабота с каталогами в PythonРабота с CSV в PythonВиртуальная среда venv в Python — создание и настройкаКак создать простое приложение на PythonИспользование pip в Python для установки пакетовМодули в Python и организация кода в проектеИмпорт модулей в Python и правила подключенияРабота с файлами в Python пошаговоЧто делает компилятор Python и как он работаетПолучение строки из модуля PythonПодключение файлов в Python с includeПеременные среды в PythonСборка проекта с помощью packaging в PythonНастройка Python сервераИспользование Python на UbuntuИспользование консоли PythonИспользование кодировок в PythonИнициализация пакетов PythonИмпорт модулей PythonИмпорт имен в PythonСреда IDLE Python и базовые возможностиЧтение и запись TXT в PythonЧтение файлов в Python с помощью open file
Удаление элементов из списка PythonТипы данных в Python — обзор и рекомендацииОсновные операции со строками в PythonМетоды str в Python и обработка текстаСписки в Python и их ключевые методыСоздание списков данных в PythonРабота со строками и символами в PythonРабота со столбцами в PythonРабота со списком значений в PythonРабота с таблицами в Python с помощью DataFrameРабота с RFR в PythonРабота с пробелами в PythonРабота с массивами в PythonРабота с кортежами tuple PythonРабота с координатами X и Y в PythonРабота с ключами в PythonРабота с элементами данных PythonРабота с двоичными числами PythonРабота с данными в PythonРабота с данными NumPy PythonРабота с большими числами в PythonРабота с битами в PythonРабота с байтами в PythonЧто такое значение в Python и как его определитьМножества в Python и операции с нимиИспользование range в Python для цикловПроверка на четность в PythonПроверка числа в PythonПреобразование типов в PythonПреобразование списка в строку PythonПреобразование числа в строку в PythonПостроение графиков в PythonОпределение индекса элемента в PythonОкругление чисел в PythonОбъединение списков в Python с помощью zipМножества в PythonМассивы в Python и отличие от списковМассив чисел в PythonКортежи данных в PythonКак вычислить сумму чисел в PythonКак получить остаток от деления в PythonКак найти следующее число в PythonИспользование Unicode в PythonТип int в Python и его особенностиИндекс списка в PythonФункции для работы со строками в PythonЭлементы Python и способы доступа к нимДоступ к элементам массива в PythonДеление чисел в PythonРабота с данными в Python на практикеКак работать с числами в Python
Открыть базу знаний

Лучшие курсы по теме

Иконка молнииНовый
изображение курса

Основы Python

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий