Классы данных и типизация. Основные варианты использования.
Короче
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.
В программировании вообще все, что МОЖЕТ быть неизменным, ДОЛЖНО быть неизменным. Мы получаем две вещи:
- Легче читать программу - нам не нужно беспокоиться об изменении значений, как только оно будет создано, оно никогда не изменится (namedtuple)
- Меньше шансов на странные ошибки
Вот почему, если данные неизменны, вы должны использовать именованный кортеж вместо класса данных
Я написал это в комментарии, но я упомяну это здесь: Вы определенно правы, что есть совпадение, особенно с 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)