Именованные кортежи. Пишем код на Python чище
- суббота, 3 июня 2017 г. в 03:14:46
В стандартной библиотеке питона содержится специализированный тип "namedtuple", который, кажется, не получает того внимания, которое он заслуживает. Это одна из прекрасных фич в питоне, которая скрыта с первого взгляда.
Именованные кортежи могут быть отличной альтернативой определению классов и они имеют некоторые другие интересные особенности, которые я хочу показать вам в этой статье.
Так что же такое именованный кортеж и что делает его таким специализированным? Хороший способ поразмышлять над этим — рассмотреть именованные кортежи как расширение над обычными кортежами.
Кортежи в питоне представляют собой простую структуру данных для группировки произвольных объектов. Кортежи являются неизменяемыми — они не могут быть изменены после их создания.
>>> 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"
Обратная сторона кортежей — это то, что мы можем получать данные из них используя только числовые индексы. Вы не можете дать имена отдельным элементам сохранённым в кортеже. Это может повлиять на читаемость кода.
Также, кортеж всегда является узкоспециализированной структурой. Тяжело обеспечить, что бы два кортежа имели одни и те же номера полей и одни и те же свойства сохранённые в них. Это делает их лёгкими для знакомства со “slip-of-the-mind” багами, где легко перепутать порядок полей.
Цель именованных кортежей — решить эти две проблемы.
Прежде всего, именованные кортежи являются неизменяемыми подобно обычным кортежам. Вы не можете изменять их после того, как вы что-то поместили в них.
Кроме этого, именованные кортежи это, эм..., именованные кортежи. Каждый объект сохраненный в них может быть доступен через уникальный, удобный для чтения человеком, идентификатор. Это освобождает вас от запоминания целочисленных индексов или выдумывания обходных путей типо определения целочисленных констант как мнемоник для ваших индексов.
Вот как выглядит именованный кортеж:
>>> from collections import namedtuple
>>> Car = namedtuple('Car' , 'color mileage')
Чтобы использовать именованный кортеж, вам нужно импортировать модуль collections
. Именованные кортежи были добавлены в стандартную библиотеку в Python 2.6. В примере выше мы определили простой тип данных "Car" с двумя полями: "color" и "mileage".
Вы можете найти синтакс немного странным здесь. Почему мы передаём поля как строку закодированную с "color mileage"?
Ответ в том, что функция фабрики именованных кортежей вызывает метод split()
на строки с именами полей. Таким образом, это, действительно, просто сокращение, что бы сказать следующее:
>>> 'color mileage'.split()
['color', 'mileage']
>>> Car = namedtuple('Car', ['color', 'mileage'])
Конечно, вы также можете передать список со строками имён полей напрямую, если вы предпочитаете такой стиль. Преимущество использования списка в том, что в этом случае легко переформатировать этот код, если вам понадобится разделить его на несколько линий:
>>> Car = namedtuple('Car', [
... 'color',
... 'mileage',
... ])
Как бы вы ни решили, сейчас вы можете создать новые объекты "car" через фабричную функцию Car
. Поведение будет такое же, как если бы вы решили определить класс Car
вручную и дать ему конструктор принимающий значения "color" и "mileage":
>>> my_car = Car('red', 3812.4)
>>> my_car.color
'red'
>>> my_car.mileage
3812.4
Распаковка кортежей и оператор '*' для распаковки аргументов функций также работают как ожидается:
>>> color, mileage = my_car
>>> print(color, mileage)
red 3812.4
>>> print(*my_car)
red 3812.4
Несмотря на доступ к значениям сохранённым в именованном кортеже через их идентификатор, вы всё ещё можете обращаться к ним через их индекс. Это свойство именованных кортежей может быть использовано для их распаковки в обычный кортеж:
>>> my_car[0]
'red'
>>> tuple(my_car)
('red', 3812.4)
Вы даже можете получить красивое строковое отображение объектов бесплатно, что сэкономит вам время и спасёт от избыточности:
>>> my_car
Car(color='red' , mileage=3812.4)
Именованные кортежи, как и обычные кортежи, являются неизменяемыми. Когда вы попытаетесь перезаписать одно из их полей, вы получите исключение AttributeError
:
>>> my_car.color = 'blue'
AttributeError: "can't set attribute"
Объекты именованных кортежей внутренне реализуются в питоне как обычные классы. Когда дело доходит до использованию памяти, то они так же "лучше", чем обычные классы и просто так же эффективны в использовании памяти как и обычные кортежи.
Хороший путь судить о них — считать, что именованные кортежи являются краткой формой для создания вручную эффективно работающего с памятью неизменяемого класса.
Поскольку именованные кортежи построены на обычных классах, то вы можете добавить методы в класс, унаследованный от именованного кортежа.
>>> 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(). Она создаёт поверхностную(shallow) копию кортежа и разрешает вам выборочно заменять некоторые поля:
>>> my_car._replace(color='blue')
Car(color='blue', mileage=3812.4)
И, наконец, метод класса _make() может быть использован чтобы создать новый экземпляр именованного кортежа из последовательности:
>>> Car._make(['red', 999])
Car(color='red', mileage=999)
Именованные кортежи могут быть простым способом для построения вашего кода, делая его более читаемым и обеспечивая лучшее структурирование ваших данных.
Я нахожу, например, что путь от узкоспециализированных структур данных с жёстко заданным форматом, таких как словари к именованным кортежам помогает мне выражать мои намерения более чисто. Часто когда я пытаюсь это рефакторить, то я магически достигаю лучших решений проблем, с которыми я сталкиваюсь.
Использование именованных кортежей вместо неструктурированных кортежей и словарей может также сделать жизнь моих коворкеров легче, потому что эти структуры данных помогают создавать данные понятным, самодокументирующимся(в достаточной степени) образом.
С другой стороны, я стараюсь не использовать именованные кортежи ради их пользы, если они не помогают мне писать чище, читабельнее и не делают код более лёгким в сопровождении. Слишком много хороших вещей может оказаться плохой вещью.
Однако, если вы используете их с осторожностью, то именованные кортежи, без сомнения, могут сделать ваш код на Python лучше и более выразительным.