
namedtuple
from collections import namedtuple
# новый класс User с полями name и phone
User = namedtuple('User', ['name', 'phone'])
Тип namedtuple определен в стандартном модуле collections. Этот класс расширяет понятие кортежа, добавляя ему и его полям имена и, таким образом, наделяя их смысловой нагрузкой. Он позволяет писать более читаемый и самодокументируемый код. nametuple можно использовать и как обычный кортеж, или получать доступ к полям по именам, а не только индексам.
Вывоз конструктора namedtuple по сути вернет новый тип с определенными настройками. Пример:
from collections import namedtuple # новый класс User с полями name и phone User = namedtuple('User', ['name', 'phone']) # конкретный юзер - экземпляр user1 = User('John', phone='+79991002030') print(user1) # User(name='John', phone='+79991002030')
Первый аргумент namedtuple – название класса, а второй – список параметров, который также может быть и строкой типа 'name, phone'
или даже 'name phone'
. Имена полей любые, кроме зарезервированных слов и имен, начинающихся с подчеркивания.
Работа с namedtuple:
>> Point = namedtuple('Point', ['x', 'y']) >> p = Point(x=11, y=22) # создание >>> p[0] + p[1] # доступ по индексам 33 >>> p.x + p.y # доступ по именам 33 >>> x, y = p # распаковка, как обычный кортеж >>> x, y (11, 22) >>> p # читабельное repr Point(x=11, y=22) >>> Point._make((22, 33)) # создание из обычного кортежа или другого итерируемого объекта Point(x=22, y=33) >>> p._asdict() # представление в форме словаря {'x': 11, 'y': 22}
Название двух последних начинаются с подчеркивания, это не значит, что мы вызываем приватные методы, это сделано намеренно, чтобы избежать конфликта с именами полей, если вдруг вам пришло бы в голову назвать поле «asdict
«.
Кроме описанных выше возможностей, namedtuple позволяет также:
1. Задавать значения поле по умолчанию
2. Добавлять док-стринги (подсказки) к самому классу и отдельным полям
3. Создавать копии кортежа с заменой полей
4. Расширять функциональность путем наследования
Значения по умолчанию для namedtuple (Python 3.7+)
from collections import namedtuple User = namedtuple('User', ['name', 'phone']) user = User(name='Jack') # ошибка! не указан phone!
Значения по умолчанию при создании namedtuple дают возможность не указывать одно или несколько полей в конструкторе непосредственных экземпляров. Значения по умолчанию задаются через параметр defaults (он всегда указывается явно по имени) в форме списка или кортежа длиной, обычно равной количеству полей в создаваемом namedtuple.
User = namedtuple('User', ['name', 'phone'], defaults=('NoName', 'NoPhone')) user = User(name='Jack') >> user User(name='Jack', phone='NoPhone') >>> User() # вообще без параметров User(name='NoName', phone='NoPhone')
Чтобы устранить исключение о недостающих полях в конструкторе, достаточно передать defaults как кортеж из None:
fields = ('val', 'left', 'right') Node = namedtuple('Node', fields, defaults=(None,) * len(fields)) >>> Node() Node(val=None, left=None, right=None)
🤸 Вообще говоря, defaults могут быть и короче, чем список полей. В таком случае значения по умолчанию применяются только к самым правым полям. Чтобы было понятно, вот пример:
Node = namedtuple('Node', ('val', 'left', 'right'), defaults=(100, 200))
Так как defaults из двух элементов, а полей – три, то они применятся только к последним полям ‘left
‘ и ‘right
‘, а ‘val
‘ останется без дефолтного значения, вынуждая нас всегда его указывать:
>>> Node() TypeError: __new__() missing 1 required positional argument: 'val' >>> Node(13) Node(val=13, left=100, right=200) >>> Node(val=42) Node(val=42, left=100, right=200)
💣 Важно! namedtuple, как и обычный tuple является неизменяемым типом (immutable), однако, вам не запрещено в качестве поля использовать изменяемый тип, к примеру, list или dict, так как кортеж содержит лишь ссылку на объект, а за содержимое самого объекта он не отвечает. Поэтому, не рекомендуется делать значением по умолчанию списки, сеты и словари, а также пользовательские изменяемые объекты.
Посмотрите, здесь все экземпляры Group будут разделять один и тот же список по умолчанию для поля ‘users
‘:
Group = namedtuple('Node', ('name', 'users'), defaults=('5B', [])) # плохо! g = Group() g.users.append('Vanya') # повлияет на g2 тоже! g2 = Group() print(g2.users) # ['Vanya']
Лучше указать None. Или создать отдельную функцию, которая каждый раз будет создавать новый пустой список:
def new_empty_group(name='5B'): return Group(name=name, users=[])
Замена поля в namedtuple
namedtuple, будучи кортежем, является неизменяемым типом. Однако, метод _replace
возвращает новый объект, в котором отредактированы выбранные поля, а все остальные равны значениям из предыдущего кортежа.
from collections import namedtuple Book = namedtuple('Book', ['id', 'title', 'authors']) book1 = Book(1, 'Игрок', 'Достоевский Ф.М.') book2 = book1._replace(id=2, title='Преступление и наказание') >>> book1 Book(id=1, title='Игрок', authors='Достоевский Ф.М.') >>> book2 Book(id=2, title='Преступление и наказание', authors='Достоевский Ф.М.')
⚠️ Метод replace делает поверхностную копию данных, то есть копирует ссылки. Если со строчками и числами все будет в порядке (они скопируются), то ссылка на список будет разделяться между обоими объектами.
Документация namedtuple
Имеется возможность снабдить сам класс и его поля документацией (doc-strings):
# сам класс Book.__doc__ += ': Hardcover book in active collection' # его поля Book.id.__doc__ = '13-digit ISBN' Book.title.__doc__ = 'Title of first printing' Book.authors.__doc__ = 'List of authors sorted by last name'
Наберите в интерпретаторе help(Book)
и увидите, что у Book есть теперь описание класса и всех полей. А также по умолчанию namedtuple добавляет кучу стандартной документации по методам класса. Вот вывод подсказок о классе:
Help on class Book in module __main__: class Book(builtins.tuple) | Book(id, title, authors) | | Book(id, title, authors): Hardcover book in active collection | | Method resolution order: | Book | builtins.tuple | builtins.object | | Methods defined here: | | __getnewargs__(self) | Return self as a plain tuple. Used by copy and pickle. | | __repr__(self) | Return a nicely formatted representation string | | _asdict(self) | Return a new dict which maps field names to their values. | | _replace(self, /, **kwds) и так далее...
Расширение namedtuple
Можно расширить функциональность namedtuple, создав класс-наследник с целью добавлять в него новые методы и свойства, не теряя всех преимуществ namedtuple. Например, для точки можно определить метод hypot, рассчитывающий расстояние от начала координат.
class Point(namedtuple('Point', ['x', 'y'])): __slots = () @property def hypot(self): return (self.x 2 + self.y 2) ** 0.5 def __str__(self): return f'Point: x={self.x} y={self.y} hypot={self.hypot}' >>> print(Point(1, 2)) Point: x=1 y=2 hypot=2.23606797749979
Однако, вы не можете менять значения полей внутри методов, так как кортеж – неизменяемый тип данных.
Существует еще один экзотический способ создать класс от namedtuple, причем с «типизированными» полями:
from typing import NamedTuple class Employee(NamedTuple): name: str id: int
Это эквивалент такой записи:
Employee = namedtuple('Employee', ['name', 'id'])
Однако, первый вариант смотрится выразительнее и помогает IDE анализировать код.
По сравнению с обычными классами namedtuple также неплохо экономит оперативную память. Вот посмотрите этот пример. Обычный класс с двумя атрибутами занимает целых 328, когда как схожий namedtuple – всего 120.
Строка __slots = ()
здесь играет особую роль. Я еще не рассказывал про слоты, но если вкратце, то таким образом мы предотвращаем создание внутреннего словаря для данного класса, что экономит еще 8 байт.
namedtuple – производная от tuple, который написан на Си, что дает дополнительный буст производительности по сравнению с классами, написанными на чистом Python.
Вывод: namedtuple позволяет быстро и удобно создавать небольшие неизменяемые классы для хранения данных. Такие классы эффективны по объему используемой памяти и уже содержат большое количество вспомогательных функций, таких как инициализация, сравнение, хэширование, представление данных, преобразование в словарь и так далее. Также, namedtuple наследует поведение обычного кортежа tuple, в том плане, что можно обратиться к полям по индексам или распаковать их в отдельные переменные.
Но не спешите переделывать все ваши классы на namedtuple, потому что:
1. namedtuple не изменяем
2. без танцев с бубнами невозможно наследование
3. много прочих проблем с ООП
Если вам нужен изменяемый аналог namedtuple, то советую присмотреться к dataclass. О нем я расскажу в будущих постах.
Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈