Реализация записей (Record) в Python
Варианты реализации типа запись (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)) # TrueTrue
Использование слотов для экономии памяти (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 в данном тесте оказался быстрее, но его нужно устанавливать отдельно. Выбор зависит от требований к производительности и удобству.