Dataclass

Ещё один вариант задания структуры — dataclass:

from dataclasses import dataclass

@dataclass
class Coordinates:
    longitude: float
    latitude: float

def get_gps_coordinates() -> Coordinates:
    return Coordinates(10, 20)

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

С ним работают проверки в IDE, автодополнения — это, пожалуй, самая часто используемая структура для таких задач:

print(get_gps_coordinates().latitude)  # Автодополнение IDE для атрибута
print(get_gps_coordinates().latitudeRRR)  # IDE подсветит опечатку

Когда использовать NamedTuple, когда dataclass? Как мы поймём чуть позже, сценарий именованных кортежей — это сценарий распаковки. Когда нам нужно использовать структуру именно как кортеж, тогда стоит задать её как NamedTuple. В остальных сценариях имеет смысл предпочесть dataclass.

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

from dataclasses import dataclass
from typing import NamedTuple

from pympler import asizeof

@dataclass
class CoordinatesDT:
    longitude: float
    latitude: float

class CoordinatesNT(NamedTuple):
    longitude: float
    latitude: float


coordinates_dt = CoordinatesDT(longitude=10.0, latitude=20.0)
coordinates_nt = CoordinatesNT(longitude=10.0, latitude=20.0)

print("dataclass", asizeof.asized(coordinates_dt).size)  # 328 bytes
print("namedtuple:", asizeof.asized(coordinates_nt).size)  # 104 bytes

То есть, как видим, именованный кортеж занимает значительно меньше памяти в оперативке, чем dataclass, в данном примере в 3 раза. Это понятно, то как по своей сути это более простая структура данных, её нельзя менять и потому именованный кортеж можно эффективно хранить в памяти.

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

from dataclasses import dataclass
from pympler import asizeof


@dataclass(slots=True, frozen=True)
class CoordinatesDT2:
    longitude: float
    latitude: float

coordinates_dt2 = CoordinatesDT2(longitude=10.0, latitude=20.0)
print("dataclass with frozen and slots:", asizeof.asized(coordinates_dt2).size)
# dataclass with frozen and slots: 96 bytes

Обрати внимание — такая структура неизменна, как и кортеж (благодаря флагу frozen=True), то есть не получится после определения экземпляра класса изменить его атрибуты. Флаг slots=True автоматически добавляет __slots__ нашему датаклассу (более быстрый доступ к атрибутам и более эффективное хранение в памяти).

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

latitude, longitude = coordinates_nt

Экземпляр датакласса, очевидно, с распаковкой работать не будет, так как это не кортеж.