Реализация записей (Record) в Python

Раздел: Структуры данных -> Record type

Варианты реализации типа запись (Record) в Python

Как создать изменяемую запись с минимальным кодом и автоматическими методами?

Наиболее эффективное решение в современном Python - использование декоратора dataclass из модуля dataclasses. Он генерирует методы __init__, __repr__, __eq__ и другие, а также поддерживает аннотации типов.

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int
    city: str = 'Moscow'

Python тип запись (тип запись (record) в python)

После этого можно создавать экземпляры:

p = Person('Alice', 30, 'SPb')
print(p)   # Person(name='Alice', age=30, city='SPb')

Поля с типом сразу видны, значения по умолчанию задаются просто. При необходимости запись можно сделать неизменяемой параметром frozen=True, а для автоматической сортировки использовать order=True.

Типичные ошибки и их решение:

  • Забыли импортировать dataclass - возникает NameError.
  • Не указали тип поля (например, просто name без аннотации) - поле не будет обработано декоратором.
  • Изменяемые значения по умолчанию (например, список) - нужно использовать фабрику field(default_factory=list).
  • Наследование от другого dataclass может привести к конфликтам полей, рекомендуется переопределять все поля явно.

Как сделать неизменяемую запись, которая ведет себя как кортеж?

Для неизменяемых записей с доступом по имени подходит NamedTuple из модуля collections. Он наследует от кортежа, поэтому экземпляры легковесны и хэшируемы.

from collections import namedtuple

Person = namedtuple('Person', ['name', 'age', 'city'])
p = Person('Bob', 25, 'Tver')

Доступ к полям через атрибуты или индексы:

print(p.name)   # Bob
print(p[0])     # Bob

Недостаток - нет автоматической проверки типов, значения по умолчанию задаются через прототип (сложнее).

Проблемы:

  • Нельзя изменить поле после создания (аттрибут только для чтения).
  • Сложно добавлять методы - нужно наследовать от NamedTuple.
  • Если много полей, описание становится громоздким.

Как создать запись без использования декораторов и наследования?

Можно написать обычный класс с явным конструктором и методом __repr__. Этот подход дает полный контроль, но требует больше кода.

class Person:
    def __init__(self, name, age, city='Moscow'):
        self.name = name
        self.age = age
        self.city = city
    def __repr__(self):
        return f'Person(name={self.name!r}, age={self.age!r}, city={self.city!r})'

Использование:

p = Person('Charlie', 35)
print(p)   # Person(name='Charlie', age=35, city='Moscow')

Ошибки: легко опечататься в названии поля, методы приходится писать вручную. При добавлении нового поля нужно не забыть обновить __init__ и __repr__.

Как использовать словарь в качестве записи?

Самый простой способ - словарь с ключами-строками. Подходит для прототипирования, но нет автодополнения и проверки орфографии.

person = {'name': 'Diana', 'age': 28, 'city': 'Kazan'}

Доступ по ключу:

print(person['name'])   # Diana

Проблемы: ключ может быть введен с опечаткой (например, 'nmae') - ошибка проявится только во время выполнения; нет возможности добавить методы; структура не фиксирована (можно случайно удалить ключ).

Как создать простую запись с доступом через атрибуты без определения класса?

Класс SimpleNamespace из модуля types позволяет динамически задавать атрибуты. Полезен для быстрых скриптов.

from types import SimpleNamespace

person = SimpleNamespace(name='Eve', age=22, city='Samara')
print(person.name)   # Eve

Недостатки: нет аннотаций, нет проверки типов, атрибуты можно случайно перезаписать или удалить.

Как аннотировать словарь как запись с фиксированными ключами?

TypedDict из модуля typing добавляет подсказки типов для словарей. Подходит для статической проверки (mypy), но не влияет на выполнение.

from typing import TypedDict

class PersonDict(TypedDict):
    name: str
    age: int
    city: str

person: PersonDict = {'name': 'Frank', 'age': 40, 'city': 'Ufa'}

При использовании несуществующего ключа mypy выдаст ошибку, но во время выполнения словарь останется обычным.

Ошибка: путаница с обычным классом - TypedDict не создает класс с атрибутами, это только аннотация для словаря.

Как создать запись с производительностью, близкой к кортежу?

Библиотека recordclass (третья сторона) предлагает изменяемые записи, которые занимают меньше памяти и быстрее, чем dataclass, особенно с опцией mutable.

from recordclass import recordclass

Person = recordclass('Person', ['name', 'age', 'city'])
p = Person('Grace', 27, 'Volgograd')

Экземпляры ведут себя как списки/кортежи с именованными полями. Можно задавать значения по умолчанию через параметр defaults.

Необходимо установить библиотеку (pip install recordclass), не входит в стандартную поставку. Может быть несовместима с некоторыми версиями Python.

Расширенные примеры и сценарии использования

Работа с вложенными записями (dataclass)

Создадим запись для адреса и вложим её в запись человека.

Пример
from dataclasses import dataclass

@dataclass
class Address:
    street: str
    city: str
    zip_code: str

@dataclass
class Employee:
    name: str
    position: str
    address: Address

e = Employee('Harry', 'Developer', Address('Lenina 10', 'Moscow', '101000'))
print(e)
# Employee(name='Harry', position='Developer', address=Address(street='Lenina 10', city='Moscow', zip_code='101000'))
Employee(name='Harry', position='Developer', address=Address(street='Lenina 10', city='Moscow', zip_code='101000'))

Сериализация записей в JSON

Dataclass можно легко преобразовать в словарь, а затем в JSON.

Пример
from dataclasses import asdict
import json

@dataclass
class Product:
    name: str
    price: float
    quantity: int = 0

p = Product('Laptop', 999.99, 5)
data = asdict(p)
json_str = json.dumps(data, ensure_ascii=False, indent=2)
print(json_str)
{
  "name": "Laptop",
  "price": 999.99,
  "quantity": 5
}

Сортировка записей с помощью order=True

Параметр order=True генерирует методы сравнения, позволяя сортировать список записей.

Пример
from dataclasses import dataclass

@dataclass(order=True)
class Student:
    grade: int
    name: str

students = [Student(5, 'Ivan'), Student(3, 'Anna'), Student(4, 'Oleg')]
students.sort()
print([s.name for s in students])
['Anna', 'Oleg', 'Ivan']

Неизменяемая запись с frozen=True и хэширование

Для использования записей в качестве ключей словаря или элементов множества нужна неизменяемость.

Пример
from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p1 = Point(1, 2)
p2 = Point(1, 2)
d = {p1: 'start', p2: 'end'}  # p1 и p2 интерпретируются как один ключ
print(d)  # {Point(x=1, y=2): 'end'}
print(hash(p1) == hash(p2))  # True
True

Использование слотов для экономии памяти (dataclass + __slots__)

Комбинация dataclass с параметром slots=True (Python 3.10+) уменьшает потребление памяти.

Пример
from dataclasses import dataclass

@dataclass(slots=True)
class SmallRecord:
    a: int
    b: str

r = SmallRecord(1, 'test')
print(r.__slots__)  # ('a', 'b')
('a', 'b')

Фабрика значений по умолчанию для изменяемых объектов

Если поле должно содержать список, используем default_factory.

Пример
from dataclasses import dataclass, field
from typing import List

@dataclass
class Team:
    name: str
    members: List[str] = field(default_factory=list)

t = Team('Alpha')
t.members.append('John')
t.members.append('Jane')
print(t)  # Team(name='Alpha', members=['John', 'Jane'])
Team(name='Alpha', members=['John', 'Jane'])

Наследование dataclass с переопределением полей

При создании подкласса важно правильно задать значения по умолчанию и типы.

Пример
from dataclasses import dataclass

@dataclass
class Base:
    x: int = 0
    y: int = 0

@dataclass
class Derived(Base):
    z: int = 1

d = Derived(1, 2, 3)
print(d)  # Derived(x=1, y=2, z=3)
Derived(x=1, y=2, z=3)

Сравнение производительности: recordclass vs dataclass vs namedtuple

Следующий код измеряет время создания экземпляров и доступ к полям (результат может отличаться на разных машинах).

Пример
import timeit
from collections import namedtuple
from dataclasses import dataclass
from recordclass import recordclass

# Определения
PersonNT = namedtuple('PersonNT', 'name age')

@dataclass
class PersonDC:
    name: str
    age: int

PersonRC = recordclass('PersonRC', 'name age')

# Создание
print('Create namedtuple:', timeit.timeit('PersonNT("A", 1)', globals=globals(), number=100000))
print('Create dataclass:', timeit.timeit('PersonDC("A", 1)', globals=globals(), number=100000))
print('Create recordclass:', timeit.timeit('PersonRC("A", 1)', globals=globals(), number=100000))

# Доступ к полю
p_nt = PersonNT('A', 1)
p_dc = PersonDC('A', 1)
p_rc = PersonRC('A', 1)
print('Access namedtuple:', timeit.timeit('p_nt.name', globals=globals(), number=1000000))
print('Access dataclass:', timeit.timeit('p_dc.name', globals=globals(), number=1000000))
print('Access recordclass:', timeit.timeit('p_rc.name', globals=globals(), number=1000000))
Create namedtuple: 0.0234
Create dataclass: 0.0351
Create recordclass: 0.0198
Access namedtuple: 0.0112
Access dataclass: 0.0145
Access recordclass: 0.0099

Обратите внимание: recordclass в данном тесте оказался быстрее, но его нужно устанавливать отдельно. Выбор зависит от требований к производительности и удобству.

Тип запись (record) в Python - comments

En
Python тип запись (python)