image_pdf

Как реализовать записи, структуры и «привычные старые объекты данных» в Python, используя только встроенные типы данных и классы из стандартной библиотеки.

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

В этой статье я довольно «вольно» обращаюсь с понятием «запись». Например, здесь также собираюсь обсудить такие типы, как встроенный tuple, который в классическом понимании из‑за отсутствия именованных полей обычно не относят к “записям”.

В Python есть несколько типов данных, которые подходят для реализации записей, структур и объектов для передачи данных. Здесь быстрый взгляд на каждый вариант реализации и их уникальные свойства. В конце вы найдете резюме и руководство, которые помогут вам сделать свой собственный выбор для решения конкретной задачи.

Хорошо, начинаем:

Встроенный тип данных – словарь (dict)

Словари Python хранят произвольное количество объектов, каждый из которых идентифицируется уникальным ключом. Словари часто также называют «картами» или «ассоциативными массивами», которые позволяют эффективно искать, вставлять и удалять любой объект, связанный с указанным ключом.

В качестве записи или объекта данных в Python можно использовать словари (dict). Словари легко создавать и для этого имеются своеобразные синтаксические «плюшки», встроенный в язык в виде словарных литералов. Синтаксис словаря лаконичен и довольно удобен для ввода.

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

car1 = {
    'color': 'red',
    'mileage': 3812.4,
    'automatic': True,
}
car2 = {
    'color': 'blue',
    'mileage': 40231.0,
    'automatic': False,
}

# У Dict есть хорошее формальное строковое представление (repr):
>>> car2
{'color': 'blue', 'automatic': False, 'mileage': 40231.0}

# Получить значение поля mileage:
>>> car2['mileage']
40231.0

# Dict можно изменить:
>>> car2['mileage'] = 12
>>> car2['windshield'] = 'broken'
>>> car2
{'windshield': 'broken', 'color': 'blue',
 'automatic': False, 'mileage': 12}

# Нет защиты от неправильных имен полей,
# или пропущенных/дополнительных полей:
car3 = {
    'colr': 'green',
    'automatic': False,
    'windshield': 'broken',
}

Встроенный тип данных – кортеж (tuple)

Кортежи Python — это простая структура данных для группировки произвольных объектов. Кортежи неизменны, т.е. являются константными после создания.

Кортежи занимают намного меньше места в памяти, чем списки, они производительней, при создании экземпляра класса работают быстрее. Как можно видеть в приведенной ниже байт‑кодовой раскладке, построение константного кортежа занимает всего один LOAD_CONST, в то время как построение объекта list с тем же содержанием требует дополнительно нескольких операций:

>>> import dis
>>> dis.dis(compile("(23, 'a', 'b', 'c')", '', 'eval'))
  1       0 LOAD_CONST           4 ((23, 'a', 'b', 'c'))
          3 RETURN_VALUE

>>> dis.dis(compile("[23, 'a', 'b', 'c']", '', 'eval'))
  1       0 LOAD_CONST           0 (23)
          3 LOAD_CONST           1 ('a')
          6 LOAD_CONST           2 ('b')
          9 LOAD_CONST           3 ('c')
         12 BUILD_LIST           4
         15 RETURN_VALUE

Однако, этим различиям не надо уделять слишком много внимания. На практике разница в производительности часто будет незначительной и попытка выжать дополнительную производительность из программы путем переключения со списков на кортежи, вероятно, будет неправильным подходом.

Потенциальным недостатком простых кортежей является то, что данные, которые вы храните в них, могут быть извлечены только путем доступа к ним через целочисленные индексы. Вы не можете дать имена отдельным свойствам, хранящимся в кортеже. Это может повлиять на читаемость кода.

Кроме того, кортеж всегда является специальной структурой. Трудно гарантировать, что два кортежа имеют одинаковое количество полей и одинаковые свойства, хранящиеся в них.

Это позволяет легко совершать ошибки «сдвига по фазе», например, путая порядок полей. Поэтому я бы рекомендовал вам минимизировать количество полей в кортеже.

# Подя: color, mileage, automatic
car1 = ('red', 3812.4, True)
car2 = ('blue', 40231.0, False)

# Есть хорошее формальное строковое представление (repr):
>>> car1
('red', 3812.4, True)
>>> car2
('blue', 40231.0, False)

# Получить mileage:
>>> car2[1]
40231.0

# Tuples не могут быть изменены:
>>> car2[1] = 12
TypeError: "'tuple' object does not support item assignment"

# Нет защиты от пропущенных/лишних полей
# или неправильного порядка следования полей:
>>> car3 = (3431.5, 'green', True, 'silver')

Создание собственного класса

Классы позволяют повторно использовать ранее созданные схемы элементов для объектов данных, что гарантирует один и тот же набор полей во всех экземплярах.

Использование обычных классов Python в качестве типов данных записи возможно, но это также требует ручной работы для получения удобных функций других реализаций. Например, добавление новых полей в конструкторе класса __init__ требует времени и внимательности.

Кроме того, формальное строковое представление (repr, по умолчанию) для объектов, созданных из пользовательских классов, не очень полезно. Чтобы исправить это, возможно, придется добавить свой собственный метод __repr__, так же обычно довольно многословен и должен обновляться каждый раз при добавлении нового поля.

Значение полей, хранящихся в классах, могут быть изменены и новые поля могут быть свободно добавлены, что может соответствовать или не соответствовать вашим намерениям. Можно обеспечить полный контроль доступа и создавать поля только для чтения с помощью декоратора @property, но для этого требуется написать больше кода.

Определение пользовательского класса является отличным вариантом всякий раз, когда вы хотите добавить бизнес‑логику и поведение для своих объектов записи с помощью специфических методов. Но это означает, что такие объекты технически уже больше не являются просто объектами данных.

class Car:
    def __init__(self, color, mileage, automatic):
        self.color = color
        self.mileage = mileage
        self.automatic = automatic

car1 = Car('red', 3812.4, True)
car2 = Car('blue', 40231.0, False)

# Получить mileage:
>>> car2.mileage
40231.0

# Изменение значений элементов в экземпляре класса:
>>> car2.mileage = 12
>>> car2.windshield = 'broken'

# Строковое представление не очень полезно
# (необходимо добавить метод __repr__, написанный вручную):
>>> car1
<Car object at 0x1081e69e8>

Класс collections.namedtuple

Класс namedtuple, доступный начиная с Python 2.6+, расширяет возможности встроенного типа tuple. Аналогично определению пользовательского класса, использование namedtuple позволяет повторно определить используемые «схемы элементов» для записей, что гарантируют использование правильных имен полей.

Так же как и обычные, именованные кортежи нельзя изменять. Это означает, что вы не сможете добавлять новые поля или изменять существующие поля после создания экземпляра namedtuple.

Кроме того, namedtuple — это, ну…, именованные кортежи. К каждому объекту, хранящемуся в них, можно получить доступ через уникальный идентификатор. Это освобождает от необходимости запоминать целочисленные индексы или прибегать к обходным путям, таким как определение целочисленных констант в качестве мнемоник для ваших индексов.

Объекты namedtuple реализованы как обычные классы Python. Когда речь заходит об использовании памяти, они также “лучше”, чем обычные классы, так же эффективней в памяти, чем обычные кортежи:

>>> from collections import namedtuple
>>> from sys import getsizeof

>>> p1 = namedtuple('Point', 'x y z')(1, 2, 3)
>>> p2 = (1, 2, 3)

>>> getsizeof(p1)
72
>>> getsizeof(p2)
72

Именованные кортежи, может быть, самый простой способ очистить свой код и сделать его более читаемым, применяя лучшую конструкцию для данных.

Я думаю, что переход от специальных типов данных, таких как словари с фиксированным форматом, к namedtuple помогает более четко выразить намерение кода. Я часто применяю этот рефакторинг, что волшебным образом способствует решению многих проблем при проектировании.

Использование namedtuple поверх неструктурированных tuple и dict также может облегчить жизнь моих коллег, потому что namedtuple делают данные, передаваемые вокруг, “самодокументированными”, по крайней мере до некоторой степени.

Для получения дополнительной информации и примеров кода посмотрите туториал по namedtuple.

from collections import namedtuple

Car = namedtuple('Car' , 'color mileage automatic')

car1 = Car('red', 3812.4, True)

# Замечательный repr:
>>> car1
Car(color='red', mileage=3812.4, automatic=True)

# Доступ к полям:
>>> car1.mileage
3812.4

# Поля изменять нельзя:
>>> car1.mileage = 12
AttributeError: "can't set attribute"
>>> car1.windshield = 'broken'
AttributeError: "'Car' object has no attribute 'windshield'"

Класс typing.NamedTuple

Этот класс, добавленный в Python 3.6 и является младшим братом collections.namedtuple. Он очень похож на namedtuple, а главное отличие заключается в обновлении синтаксиса при определения новых типов записей, добавлена поддержка подсказок типа данных.

Обратите внимание, что аннотации типов не применяются без отдельного инструмента проверки типов, такого как type checking tool like mypy, но даже без поддержки инструментов они могут предоставлять полезные подсказки другим программистам (или быть ужасно запутанными, если подсказки типа устарели.)

from typing import NamedTuple

class Car(NamedTuple):
    color: str
    mileage: float
    automatic: bool

car1 = Car('red', 3812.4, True)

# Instances have a nice repr
>>> car1
Car(color='red', mileage=3812.4, automatic=True)

# Accessing fields
>>> car1.mileage
3812.4

# Fields are immutable
>>> car1.mileage = 12
AttributeError: "can't set attribute"
>>> car1.windshield = 'broken'
AttributeError: "'Car' object has no attribute 'windshield'"

# Type annotations are not enforced without
# a separate type checking tool like mypy:
>>> Car('red', 'NOT_A_FLOAT', 99)
Car(color='red', mileage='NOT_A_FLOAT', automatic=99)

Класс struct.Struct

Этот класс выполняет преобразования между значениями Python и C-структурами, сериализованными в объекты bytes Python. Например, он может быть использован для обработки двоичных данных, хранящихся в файлах или при сетевых соединениях.

Структуры определяются с помощью формата strings-like mini language, который позволяет определить расположение различных типов данных C, таких char, int, и long, а также их вариант unsigned.

Модуль struct редко используется для представления объектов данных, которые предназначены для обработки исключительно внутри кода Python. Они предназначены в первую очередь как формат обмена данными, а не способа хранения данных в памяти для кода Python.

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

from struct import Struct

MyStruct = Struct('i?f')

data = MyStruct.pack(23, False, 42.0)

# All you get is a blob of data:
>>> data
b'\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00(B'

# Data blobs can be unpacked again:
>>> MyStruct.unpack(data)
(23, False, 42.0)

Класс types.SimpleNamespace

Вот еще один «эзотерический» выбор для реализации объектов данных в Python. Этот класс был добавлен в Python 3.3 и обеспечивает доступ к атрибутам пространства имен. Он также включает в себя значимый __repr__ по умолчанию.

Как следует из названия, SimpleNamespace — это простой именованный словарь с доступом по атрибутам, которые красиво печатаются. Атрибуты могут быть свободно добавлены, изменены и удалены.

from types import SimpleNamespace
car1 = SimpleNamespace(color='red', mileage=3812.4, automatic=True)

# repr по умолчанию:
>>> car1
namespace(automatic=True, color='red', mileage=3812.4)

# Можно изменять
>>> car1.mileage = 12
>>> car1.windshield = 'broken'
>>> del car1.automatic
>>> car1
namespace(color='red', mileage=12, windshield='broken')

Какую конструкцию надо использовать для объектов данных в Python?

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

  • У вас есть всего несколько (2-3) полей: использование простого кортежа может быть оправдано, потому что порядок полей легко запомнить или имена полей излишни. Например, описание точки в трёхмерном пространстве (x, y, z).
  • Вам нужны неизменяемые поля: в этом случае простые кортежи, collections.namedtuple, typing.NamedTuple были бы хорошими вариантами для реализации подобного объекта данных.
  • Вам нужно заблокировать имена полей, чтобы избежать опечаток: collections.namedtuple и typing.NamedTuple ваши друзья.
  • Вы хотите, чтобы все было просто: простой объект словаря может быть хорошим выбором из-за удобного синтаксиса, который близко напоминает JSON.
  • Вам нужен полный контроль над своей структурой данных: пришло время написать пользовательский класс с декоратором @property, сеттерами и геттерами.
  • Вам нужно добавить поведение (методы) к объекту: вы должны написать пользовательский класс. Либо с нуля, либо путем расширения collections.namedtuple или typing.NamedTuple.
  • Вам нужно плотно упаковать данные для сериализации их на диск или отправки по сети: самое время применить struct.Struct, который является отличным вариантом для этого случая.

Если ключевым моментом выбора является безопасность, то рекомендую для реализации простой записи, структуры или объекта данных в Python использовать:

  • collections.namedtuple в Python 2.x; и
  • его младшего брата typing.NamedTuple в Python 3.

Ознакомьтесь с полной серией статей «Основные структуры данных в Python». В этой статье чего-то не хватает или вы нашли ошибку? Помогите коллеге и оставьте комментарий ниже.

Source

Опубликовано Вадим В. Костерин

Ст.преп. кафедры ИТЭ. Автор более 130 научных и учебно-методических работ. Лауреат ВДНХ (серебряная медаль).

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *