Главная » Хабрахабр » Кортеж здорового человека

Кортеж здорового человека

Мы рассмотрим его приятные особенности, от известных до неочевидных. Именованный кортеж
Эта статья — об одном из лучших изобретений Python: именованном кортеже (namedtuple). Поехали! Уровень погружения в тему будет нарастать постепенно, так что, надеюсь, каждый найдёт для себя что-то интересное.

Введение

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

Часто создавать отдельный класс под это дело лень, и используют кортежи:

("pigeon", "Френк", 3)
("fox", "Клер", 7)
("parrot", "Питер", 1)

Для большей наглядности подойдёт именованный кортеж — collections.namedtuple:

from collections import namedtuple Pet = namedtuple("Pet", "type name age")
frank = Pet(type="pigeon", name="Френк", age=3) >>> frank.age
3

Это все знают ツ А вот несколько менее известных особенностей:

Быстрое изменение полей

Френк стареет, а кортеж-то неизменяемый. Что делать, если одно из свойств надо изменить? Чтобы не пересоздавать его целиком, придумали метод _replace():

>>> frank._replace(age=4)
Pet(type='pigeon', name='Френк', age=4)

А если хотите сделать всю структуру изменяемой — _asdict():

>>> frank._asdict()
OrderedDict([('type', 'pigeon'), ('name', 'Френк'), ('age', 3)])

Автоматическая замена названий

Названия полей взяли из заголовка CSV-файла. Допустим, вы импортируете данные из CSV и превращаете каждую строчку в кортеж. Но что-то идёт не так:

# headers = ("name", "age", "with")
>>> Pet = namedtuple("Pet", headers)
ValueError: Type names and field names cannot be a keyword: 'with' # headers = ("name", "age", "name")
>>> Pet = namedtuple("Pet", headers)
ValueError: Encountered duplicate field name: 'name'

Решение — аргумент rename=True в конструкторе:

# headers = ("name", "age", "with", "color", "name", "food")
Pet = namedtuple("Pet", headers, rename=True) >>> Pet._fields
('name', 'age', '_2', 'color', '_4', 'food')

«Неудачные» названия переименовались в соответствии с порядковыми номерами.

Значения по умолчанию

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

Pet = namedtuple("Pet", "type name alt_name") >>> Pet("pigeon", "Френк")
TypeError: __new__() missing 1 required positional argument: 'alt_name' >>> Pet("pigeon", "Френк", None)
Pet(type='pigeon', name='Френк', alt_name=None)

Чтобы этого избежать, укажите в конструкторе аргумент defaults:

Pet = namedtuple("Pet", "type name alt_name", defaults=("нет",)) >>> Pet("pigeon", "Френк")
Pet(type='pigeon', name='Френк', alt_name='нет')

Работает в питоне 3. defaults присваивает умолчательные значения с хвоста. 7+

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

Pet = namedtuple("Pet", "type name alt_name")
default_pet = Pet(None, None, "нет") >>> default_pet._replace(type="pigeon", name="Френк")
Pet(type='pigeon', name='Френк', alt_name='нет') >>> default_pet._replace(type="fox", name="Клер")
Pet(type='fox', name='Клер', alt_name='нет')

Но с defaults, конечно, куда приятнее.

Необычайная лёгкость

Армия из ста тысяч голубей займёт всего 10 мегабайт: Одно из преимуществ именованного кортежа — легковесность.

from collections import namedtuple
import objsize # 3rd party Pet = namedtuple("Pet", "type name age")
frank = Pet(type="pigeon", name="Френк", age=None) pigeons = [frank._replace(age=idx) for idx in range(100000)] >>> round(objsize.get_deep_size(pigeons)/(1024**2), 2)
10.3

Для сравнения, если Pet сделать обычным классом, аналогичный список займёт уже 19 мегабайт.

Так происходит, потому что обычные объекты в питоне таскают с собой увесистый дандер __dict__, в котором лежат названия и значения всех атрибутов объекта:

class PetObj: def __init__(self, type, name, age): self.type = type self.name = name self.age = age frank_obj = PetObj(type="pigeon", name="Френк", age=3) >>> frank_obj.__dict__

Объекты-namedtuple же лишены этого словаря, а потому занимают меньше памяти:

frank = Pet(type="pigeon", name="Френк", age=3) >>> frank.__dict__
AttributeError: 'Pet' object has no attribute '__dict__' >>> objsize.get_deep_size(frank_obj)
335 >>> objsize.get_deep_size(frank)
239

Читайте дальше ツ Но как именованному кортежу удалось избавиться от __dict__?

Богатый внутренний мир

Если вы давно работаете с питоном, то наверняка знаете: легковесный объект можно создать через дандер __slots__:

class PetSlots: __slots__ = ("type", "name", "age") def __init__(self, type, name, age): self.type = type self.name = name self.age = age frank_slots = PetSlots(type="pigeon", name="Френк", age=3)

«Френк на слотах» такой же лёгкий, как «Френк на кортеже», смотрите: У «слотовых» объектов нет словаря с атрибутами, поэтому они занимают мало памяти.

>>> objsize.get_deep_size(frank)
239 >>> objsize.get_deep_size(frank_slots)
231

Как вы помните, конкретные классы-кортежи объявляются динамически: Если вы решили, что namedtuple тоже использует слоты — это недалеко от истины.

Pet = namedtuple("Pet", "type name age")

Конструктор namedtuple применяет разную тёмную магию и генерит примерно такой класс (сильно упрощаю):

class Pet(tuple): __slots__ = () type = property(operator.itemgetter(0)) name = property(operator.itemgetter(1)) age = property(operator.itemgetter(2)) def __new__(cls, type, name, age): return tuple.__new__(cls, (type, name, age))

То есть наш Pet — это обычный tuple, к которому гвоздями приколотили три метода-свойства:

  • type возвращает нулевой элемент кортежа
  • name — первый элемент кортежа
  • age — второй элемент кортежа

В результате Pet и занимает мало места, и может использоваться как обычный кортеж: А __slots__ нужен только для того, чтобы объекты получились лёгкими.

>>> frank.index("Френк")
1 >>> type, _, _ = frank
>>> type 'pigeon'

Хитро придумано, а?

Не уступает дата-классам

В питоне 3. Раз уж мы заговорили о генерации кода. 7 появился убер-генератор кода, которому нет равных — дата-классы (dataclasses).

Когда впервые видишь дата-класс, хочется перейти на новую версию языка только ради него:

from dataclasses import dataclass @dataclass
class PetData: type: str name: str age: int

Но есть нюанс — он толстый: Чудо как хорош!

frank_data = PetData(type="pigeon", name="Френк", age=3) >>> objsize.get_deep_size(frank_data)
335 >>> objsize.get_deep_size(frank)
239

Так что если вы начитываете из базы вагон строк и превращаете их в объекты, дата-классы — не лучший выбор. Дата-класс генерит обычный питонячий класс, объекты которого изнемогают под тяжестью __dict__.

Может тогда он станет легче? Но постойте, дата-класс ведь можно «заморозить», как кортеж.

@dataclass(frozen=True)
class PetFrozen: type: str name: str age: int frank_frozen = PetFrozen(type="pigeon", name="Френк", age=3) >>> objsize.get_deep_size(frank_frozen)
335

Даже замороженный, он остался обычным увесистым объектом со словарём атрибутов. Увы. Так что если вам нужны лёгкие неизменяемые объекты (которые к тому же можно использовать как обычные кортежи) — namedtuple по-прежнему лучший выбор.

⌘ ⌘ ⌘

Мне очень нравится именованный кортеж:

  • честный iterable,
  • динамическое объявление типов,
  • именованный доступ к атрибутам,
  • лёгкий и неизменяемый.

Что ещё надо для счастья ツ И при этом реализован в 150 строк кода.

Если хотите узнать больше о стандартной библиотеке Python — подписывайтесь на канал @ohmypy


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

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

*

x

Ещё Hi-Tech Интересное!

[Перевод] Python Testing с pytest. Начало работы с pytest, Глава 1

Вернуться Дальше Это уже приносит мне дивиденды в моей компании.Chris ShaverVP of Product, Uprising Technology Я обнаружил, что Python Testing с pytest является чрезвычайно полезным вводным руководством к среде тестирования pytest. 6 и pytest 3. Примеры в этой книге написаны ...

[Перевод] Python Testing с pytest. ГЛАВА 3 pytest Fixtures

Вернуться Дальше Эта книга — недостающая глава, отсутствующая в каждой всеобъемлющей книге Python. Frank RuizPrincipal Site Reliability Engineer, Box, Inc. 6 и pytest 3. Примеры в этой книге написаны с использованием Python 3. pytest 3. 2. 6, 2. 2 поддерживает ...