В стандартной библиотеке питона содержится специализированный тип "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 лучше и более выразительным.


Что нужно запомнить


  • collections.namedtuple — краткая форма для создания вручную эффективно работающего с памятью неизменяемого класса.
  • Именованные кортежи могут помочь сделать ваш код чище, обеспечивая вас более простыми в понимании структурами данных.
  • Именованные кортежи предоставляют несколько полезных вспомогательных методов которые начинаются с символа подчёркивания (_), но являются частью открытого интерфейса. Использовать их — это нормальная практика.
Поделиться с друзьями
-->

Комментарии (13)


  1. tmnhy
    02.06.2017 09:59
    +4

    Вопрос: Когда стоит использовать именованные кортежи?
    Ответ: Тогда, когда они помогают писать чище, читабельнее и делают код более лёгким в сопровождении, ну и ваще я так щитаю.

    Достойно КО.

    Интересно посмотреть на пример, когда namedtuple сделали код чище по сравнению со словарями.


    1. iroln
      02.06.2017 10:13

      На счёт чище не уверен, хотя бы это уродство с нарушением конвенции именования методов


      In addition to the methods inherited from tuples, named tuples support three additional methods and two attributes. To prevent conflicts with field names, the method and attribute names start with an underscore.

      А методы index и count конфликтов не могут создать?


      Но вот то, что namedtuple легче чем словарь или экземпляр любого пользовательского класса — это факт.


      Named tuple instances do not have per-instance dictionaries, so they are lightweight and require no more memory than regular tuples.

      А также с ним удобно возвращать кортежи из функций.


      1. Norraxx
        02.06.2017 20:15

        Какова выгода именных кортежей в сравнении с __slots__ ?
        https://docs.python.org/3/reference/datamodel.html#slots


        1. iroln
          02.06.2017 20:19

          Думаю, что сравнимо. Про __slots__ я не стал уж упоминать, довольно экзотическая штука. А вы часто это используете?


          1. Norraxx
            02.06.2017 20:41

            Там, где я работаю, проблемы с недостатком памяти обычно не возникают.
            А если есть проблемы с памятью, то решаются с помощью других подходов (например. yield/delete).
            Когда заканчивается память или нехватает процессорного времени, то это обозначает, что «что-то вы делаете не правильно».


  1. kx13
    02.06.2017 10:11

    Вид car.color выглядит гораздо компактнее, чем car['color'].


    При опечатке легче найти ошибку. Если вы написали car.coloor, то IDE может вам показать ошибки, с текстовыми ключами car['coloor'] такое может не получится.


    1. foldr
      04.06.2017 02:34

      В последних версиях pycharm есть автокомплит и инспекция по ключам словаря. Но читабельность, да, у namedtuple выше, плюс иммутабельность


  1. Andy_U
    02.06.2017 13:31

    Лично меня тут сильно раздражает необходимость дублирования имени класса (слева от знака присваивания и в качестве первого аргумента namedtuple). На самом-то деле они могут и не совпадать, например, после рефакторинга в Pycharm, если не поставить галочку насчет поиска в строках и комментариях.

    Код ниже рабочий:

    from collections import namedtuple
    Machine = namedtuple('Car', ['a', 'b'])
    machine = Machine(1, 2)
    print(machine)
    


    Только выводит:

    Car(a=1, b=2)
    


    Т.е. хотелось бы интеграции этого механизма прямо в интерпретаторе, а не «сбоку», как сейчас.


  1. tikhenko
    02.06.2017 14:19
    +3

    Если есть желание использовать type hints, то в 3.6 для этого есть свой синтаксис:

    from typing import NamedTuple
    
    class Man(NamedTuple):
        name: str
        age: int
        weight: float
    


    1. Ikors
      02.06.2017 21:25

      А с 3.6.1 ещё и методы стало можно добавлять без лишних плясок:


      class Vector(NamedTuple):
          x: float = 0.0
          y: float = 0.0
      
          def scale(self, amount: float) -> 'Vector':
              return Vector(self.x * amount, self.y * amount)


      1. zitryss
        04.06.2017 14:27
        +1

        А каково преимущество данного метода по сравнению с обычным классом без наследования?


        1. Ikors
          04.06.2017 18:45
          +1

          Я не уверен, что правильно понял ваш вопрос.


          Если вы о том, какие преимущества у класса, отнаследованного от NamedTuple по сравнению с обычным, то, на мой взгляд, основное преимущество в том, что он immutable. Плюс в качестве бонуса мы получаем __repr__, __eq__ и __hash__.


          Т.е., это своеобразный аналог case class из Scala или data class из Kotlin.


    1. Andy_U
      03.06.2017 13:54

      Спасибо, как-то прозевал появление этой возможности. Вот еще-бы Pycharm при вызове конструктора такого класса не подсвечивал правильные аргументы как ошибочные… Впрочем, судя по PY-22102, ошибку обещают исправить в 2017.2