В Python есть особый тип контейнера - именованные кортежи (Namedtuples), которые незаслуженно обделены вниманием разработчиков, хотя этого вполне себе заслуживают. Это одна из тех волшебных “фишек” Python, ценность которой, что называется, спрятана у всех на виду.

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

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

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

>>> tup = ('hello', object(), 42)
>>> tup
('hello', <object object at 0x105e76b70>, 42)
>>> tup[2]
42
>>> tup[2] = 23
TypeError:
"'tuple' object does not support item assignment"

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

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

Именованные кортежи в помощь

С помощью именованных кортежей решается две проблемы.

Прежде всего именованные кортежи это неизменяемые контейнеры, такие же, как регулярные кортежи. Однажды помещенные в именованный кортеж данные невозможно изменить позднее, после создания кортежа. Все атрибуты объекта, помещенного в именованный кортеж следуют простому принципу - “один раз записал, много раз прочитал”.

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

Собственно именованный кортеж выглядит следующим образом:

>>> from collections import namedtuple
>>> Car = namedtuple('Car' , 'color mileage')

Именованные кортежи были добавлены в стандвртную библиотеку Python 2.6. Для их использования необходимо импортировать модуль collections. В примере выше определен простой тип данный Car (автомобиль) с двумя полями: color (цвет) и mileage (пробег).

Вы можете удивиться почему строка Car передана как первый аргумент в функцию инициализации именованного кортежа в этом примере.

Этот параметр в документации Python именуется как typename - имя типа. Фактически это имя нового класса, который был создан вызовом функции инициализации именованного кортежа.

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

И есть еще одна синтаксическая странность в этом примере - почему мы передали свойства нового класса одной строкой color mileage?

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

>>> 'color mileage'.split()
['color', 'mileage']
>>> Car = namedtuple('Car', ['color', 'mileage'])

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

>>> Car = namedtuple('Car', [
... 'color',
...     'mileage',
... ]) Вне зависимости от того, что вы решите, теперь вы cможете создать новый объект `car` с помощью функции инициализации класса `Car`. В данном случае все происходит ровно так же как если бы вы создали класс `Car` вручную и передали в конструктор значения свойств `color` и `mileage`:

>>> my_car = Car('red', 3812.4) 
>>> my_car.color
'red'
>>> my_car.mileage
3812.4

При этом в именованных кортежах сохранена возможность доступа к значениям свойств через их индекс. Таким образом именованные кортежы можно использовать как замену для регулярных кортежей:

>>> my_car[0]
'red'
>>> tuple(my_car) 
('red', 3812.4)

Распаковка кортежа и оператор *- для распаковки аргументов функции также работают ожидаемым образом:

>>> color, mileage = my_car 
>>> print(color, mileage) 
red 3812.4
>>> print(*my_car)
red 3812.4

Для именованныйх кортежей можно получить строковое представление как объекта:

>>> my_car
Car(color='red' , mileage=3812.4)

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

>>> my_car.color = 'blue' 
AttributeError: "can't set attribute"

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

“Субклассирование” (наследование) именованных кортежей

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

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

class MyCarWithMethods(Car): 
	def hexcolor(self):
		if self.color == 'red':
			return '#ff0000'
		else:
			return '#000000'

Мы можем создать теперь экземпляр класса MyCarWithMethods и вызвать его метод hexcolor() так, как если бы мы работали с экземпляром регулярного класса:

>>> c = MyCarWithMethods('red', 1234) 
>>> c.hexcolor()
'#ff0000'

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

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

>>> Car = namedtuple('Car', 'color mileage')
>>> ElectricCar = namedtuple(
... 'ElectricCar', Car._fields + ('charge',))

Такое решение даст нам следующий результат:

>>> ElectricCar('red', 1234, 45.0) 
ElectricCar(color='red', mileage=1234, charge=45.0)

Встроенные вспомогательные методы

Вместе со свойством _fields каждый экземпляр именованного кортежа располагает другими вспомогательными методами, которые вы можете найти полезными. Их имена начинаются с символа нижнего подчеркивания (_), который обычно используется в случаях если свойство или метод класса относятся к приватным и не являются частью публичного интерфейса класса или модуля.

В случае с именованными кортежами значение символа _ имеет иной смысл. Эти вспомогательные методы и свойства являются частью общедоступного интерфейса именованных кортежей. Они были названы таким образом, чтобы избежать возможных конфликтов с областью имен пользовательтских свойств и методов именованных кортежей. Так что продолжайте использовать их, если они вам понадобятся!

Вот несколько полезных сценариев использования вспомогательных методов именованных кортежей. Например метод _asdict(). Он возвращает содержание именованного кортежа как словаря:

>>> my_car._asdict()
OrderedDict([('color', 'red'), ('mileage', 3812.4)])

Этот метод может быть полезен, если необходимо сгенерировать вывод данных в формате JSON, потому, что избавит от необходимости формировать словарь вручную:

>>> json.dumps(my_car._asdict()) 
'{"color": "red", "mileage": 3812.4}'

Другой полезный вспомогательный метод _replace(). Он создает поверхностную копию кортежа и дает возможность выборочно заменить определенные совйства.

>>> my_car._replace(color='blue') 
Car(color='blue', mileage=3812.4)

Последний метод, стоящий упоминания - метод _make(). Может использоваться для создания нового экземпляра именованного кортежа из последовательности или итерации:

>>> Car._make(['red', 999]) 
Car(color='red', mileage=999)

Когда следует использовать именованые кортежи

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

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

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

Основные выводы:

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

• Именованные кортежи могут помочь в создании чистого кода и легкогй в понимании структуры ваших данных.

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

Источник: “Python Tricks The Book” Dan Bader