Классы данных и типизация. Основные варианты использования.

Короче

PEP-557 ввел классы данных в стандартную библиотеку Python, которые в основном могут выполнять ту же роль, что и collections.namedtuple а также typing.NamedTuple, И теперь мне интересно, как разделить сценарии использования, в которых namedtuple все еще является лучшим решением.

Преимущества классов данных перед NamedTuple

Конечно, весь кредит идет на dataclass если нам нужно:

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

Преимущества классов данных кратко объяснены в том же PEP: почему бы просто не использовать namedtuple.

Q: В каких случаях namedtuple все еще лучший выбор?

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

пример

Давайте рассмотрим следующую ситуацию:

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

Подход NamedTuple:

from typing import NamedTuple

PageDimensions = NamedTuple("PageDimensions", [('width', int), ('height', int)])

Подход DataClass:

from dataclasses import dataclass

@dataclass
class PageDimensions:
    width: int
    height: int

Какое решение предпочтительнее и почему?

PS Вопрос ни в коем случае не дублирует этот вопрос, потому что здесь я спрашиваю о случаях, в которых namedtuple лучше, а не о разнице (я проверил документы и источники перед тем, как спросить)

7 ответов

Решение

Это зависит от ваших потребностей. У каждого из них есть свои преимущества.

Вот хорошее объяснение классов данных на PyCon 2018 Раймонд Хеттингер - Классы данных: генератор кода для завершения всех генераторов кода

В Dataclass вся реализация написана на Python, как и в Namedtuple, все эти варианты поведения бесплатны, потому что Namedtuple наследуется от кортежа. А структура кортежей написана на C, поэтому стандартные методы работают быстрее в Namedtuple (хэш, сравнение и т. Д.).

Но Dataclass основан на dict как Namedtuple, основанном на кортеже. В соответствии с этим у вас есть преимущества и недостатки использования этих структур. Например, использование пространства меньше в NamedTuple, но время доступа быстрее в Dataclass.

Пожалуйста, посмотрите мой эксперимент:

In [33]: a = PageDimensionsDC(width=10, height=10)

In [34]: sys.getsizeof(a) + sys.getsizeof(vars(a))
Out[34]: 168

In [35]: %timeit a.width
43.2 ns ± 1.05 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

In [36]: a = PageDimensionsNT(width=10, height=10)

In [37]: sys.getsizeof(a)
Out[37]: 64

In [38]: %timeit a.width
63.6 ns ± 1.33 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

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

from operator import itemgetter

class_namespace = {
...
    'width': property(itemgetter(0, doc="Alias for field number 0")),
    'height': property(itemgetter(0, doc="Alias for field number 1"))**
}

В каких случаях namedtuple все еще является лучшим выбором?

Когда ваша структура данных должна / может быть неизменной, хешируемой, повторяемой, неупаковываемой и сопоставимой, тогда вы можете использовать NamedTuple. Если вам нужно что-то более сложное, например, возможность наследования для вашей структуры данных, используйте Dataclass.

В программировании вообще все, что МОЖЕТ быть неизменным, ДОЛЖНО быть неизменным. Мы получаем две вещи:

  1. Легче читать программу - нам не нужно беспокоиться об изменении значений, как только оно будет создано, оно никогда не изменится (namedtuple)
  2. Меньше шансов на странные ошибки

Вот почему, если данные неизменны, вы должны использовать именованный кортеж вместо класса данных

Я написал это в комментарии, но я упомяну это здесь: Вы определенно правы, что есть совпадение, особенно с frozen=True в классах данных - но все еще есть такие функции, как распаковка, принадлежащая именованным кортерам, и она всегда неизменна - я сомневаюсь, что они удаляют именованные кортежи как таковые

У меня был тот же вопрос, поэтому я провел несколько тестов и задокументировал их здесь:

https://shayallenhill.com/python-struct-options/

Суть в том, что namedtuple лучше распаковывать, разбирать и определять размер. Dataclass быстрее и гибче.

Различия не огромны, и я бы не стал проводить рефакторинг стабильного кода для перехода от одного к другому.

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

      >>> from collections import namedtuple
>>> A = namedtuple('A', ())
>>> B = namedtuple('B', ())
>>> a = A()
>>> b = B()
>>> a == b
True

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

Еще одно важное ограничение NamedTuple в том, что он не может быть общим:

      import typing as t
T=t.TypeVar('T')
class C(t.Generic[T], t.NamedTuple): ...

TypeError: Multiple inheritance with NamedTuple is not supported

Один из вариантов использования для меня - это фреймворки, которые не поддерживают dataclasses. В частности, TensorFlow. Там tf.function может работать с typing.NamedTuple но не с dataclass.

      class MyFancyData(typing.NamedTuple):
  some_tensor: tf.Tensor
  some_other_stuf: ...

@tf.function
def train_step(self, my_fancy_data: MyFancyData):
    ...

Между ними есть еще одно небольшое различие, о котором пока не упоминалось. Доступ к атрибутам именованных кортежей можно получить по их именам и индексам, а к атрибутам классов данных — только по именам их атрибутов. Я столкнулся с этой разницей при сортировке списка объектов.

Для именованных кортежей мы можем использовать какitemgetterи вспомогательные функции. Для классов данных мы можем использовать толькоattrgetterфункция.

      #!/usr/bin/python

from typing import NamedTuple
from operator import itemgetter, attrgetter
# from dataclasses import dataclass

# @dataclass(frozen=True)
# class City:
#     cid: int
#     name: str
#     population: int

class City(NamedTuple):
    cid: int
    name: str
    population: int

c1 = City(1, 'Bratislava', 432000)
c2 = City(2, 'Budapest', 1759000)
c3 = City(3, 'Prague', 1280000)
c4 = City(4, 'Warsaw', 1748000)
c5 = City(5, 'Los Angeles', 3971000)
c6 = City(6, 'Edinburgh', 464000)
c7 = City(7, 'Berlin', 3671000)

cities = [c1, c2, c3, c4, c5, c6, c7]

sorted_cities = sorted(cities, key=attrgetter('name'))

for city in sorted_cities:
    print(city)

print('---------------------')

sorted_cities = sorted(cities, key=itemgetter(2))

for city in sorted_cities:
    print(city)
Другие вопросы по тегам