Краткая история классов данных в python

В этом руководстве мы рассмотрим различные способы создания классов данных в python, начиная с самых старых и заканчивая новыми. Надеемся, что в конце вы убедитесь в необходимости использовать pydantic dataclasses как способ создания классов данных по умолчанию.
классы.

Создание классов в python по умолчанию

Самым старым способом определения класса в python является специальный метод __init__. Для нашего примера мы будем работать с классом Point, принимающим в качестве входных данных координаты x и y.

class Point:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y


p1 = Point(1, 2)
print(p1.x)  # will print 1
print(p1)  # will print something like <__main__.Point object at 0x7fc5d4283c90>

p2 = Point(1, 2)
print(p1 == p2)  # will print False
Вход в полноэкранный режим Выход из полноэкранного режима

Как вы можете видеть, здесь присутствует некоторый код, поскольку мы определяем одни и те же переменные как аргументы метода и атрибуты класса (x и y). Когда мы пытаемся распечатать созданный нами объект, чтобы посмотреть, как он выглядит, мы видим странное представление по умолчанию, сделанное python.
Хуже того, мы можем инстанцировать объект, подобный следующему Point(1, 'foo'), и python вообще не будет жаловаться.
Можно сказать, что такие инструменты, как mypy или pyright, помогут вам поймать ошибку, но не все хотят их использовать, поэтому приходится искать другой способ.
Также в реализации класса по умолчанию не реализованы методы сравнения, поэтому p == p2 возвращает False даже если атрибуты у двух объектов имеют одинаковые значения. 🥲
Вот как мы можем исправить эти три проблемы с помощью следующего кода.

class Point:
    def __init__(self, x: int, y: int):
        for item in [x, y]:
            if not isinstance(item, int):
                raise ValueError(f'{item} is not an integer')
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Point(x={self.x}, y={self.y})'

    def __eq__(self, other):
        if other.__class__ is self.__class__:
            return (self.x, self.y) == (other.x, other.y)
        return NotImplemented

    # we take the opportunity to write the opposite method
    def __ne__(self, other):
        result = self.__eq__(other)
        if result is NotImplemented:
            return NotImplemented
        return not result


p1 = Point(1, 2)
print(p1)  # will print Point(x=1, y=2)

p2 = Point(1, 2)
print(p1 == p2)  # will print True
Point(1, 'foo')  # will raise ValueError
Вход в полноэкранный режим Выйти из полноэкранного режима

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

Namedtuples

Я сделаю небольшое отступление по поводу
именованных кортежей, потому что кто-то может возразить, что это способ быстро объявить класс, и да, это так, но не без некоторых оговорок…
Рассмотрим следующий пример:

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])
p1 = Point(1, 2)
print(p1)  # will print Point(x=1, y=2)

p2 = Point(1, 'foo')  # we can still mess with variable type

print(p1 == (1, 2))  # will print True :(
Войти в полноэкранный режим Выход из полноэкранного режима

Хорошо, с namedtuples мы имеем красивое представление по умолчанию, но мы страдаем от отсутствия проверки типов, как и в стандартном способе создания классов, и поскольку он наследует класс tuple, сравнение с кортежем в последней инструкции будет корректным, что не всегда то, что мы хотим.

attrs

Принимая во внимание проблемы, связанные с созданием классов, о которых говорилось выше, известный питонист решил предложить решение с помощью библиотеки под названием attrs. Давайте посмотрим, как мы можем переписать наш класс Point.

from attrs import define, field, validators


@define
class Point:
    x: int = field(validator=[validators.instance_of(int)])
    y: int = field(validator=[validators.instance_of(int)])


p1 = Point(1, 2)
print(p1)  # will print Point(x=1, y=2)

p2 = Point(1, 2)
print(p1 == p2)  # will print True

print(p1 == (1, 2))  # will print False

p = Point(1, 'foo')  # will raise a TypeError
Вход в полноэкранный режим Выход из полноэкранного режима

Итак, мы ясно видим разницу с классом, который мы написали от руки выше. У нас есть:

  • Красивое представление по умолчанию
  • Проверка типа с помощью функции поля и validators.
  • Реализация сравнения по умолчанию, учитывающая тип сравниваемых объектов. Вот почему тест с atuple возвращает False.

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

dataclasses

В python3.7 язык вводит dataclasses, определенные в PEP 557 с целью упростить написание классов. На самом деле, эта новая стандартная библиотека в значительной степени вдохновлена attrs. Давайте посмотрим, как выглядит наш знаменитый класс Point:

from dataclasses import dataclass


@dataclass
class Point:
    x: int
    y: int


p1 = Point(1, 2)

p2 = Point(1, 2)
print(p1 == p2)  # will print True

print(p1 == (1, 2))  # will print False

p = Point(1, 'foo')  # will not raise a TypeError :(
Вход в полноэкранный режим Выход из полноэкранного режима

Мы имеем почти те же преимущества, что и определение attrs, за исключением того, что тип аргумента не проверяется во время инициализации.
Единственный способ добиться такой проверки — сделать следующее:

from dataclasses import dataclass


@dataclass
class Point:
    x: int
    y: int

    def __post_init__(self):
        if not isinstance(self.x, int):
            raise TypeError('x is not an integer')
        if not isinstance(self.y, int):
            raise TypeError('y is not an integer')


Point(1, 'foo')  # will raise a TypeError
Войти в полноэкранный режим Выйти из полноэкранного режима

Да, это немного отстойно, но такова была воля сопровождающих CPython — иметь упрощенную версию attrs без валидации и прочих радостей.

pydantic dataclasses

Наконец, мы поговорим о pydantic, библиотеке валидации данных, ставшей известной благодаря
относительно молодым веб-фреймворком FastAPI. Если вы не знакомы с ним, я настоятельно рекомендую ознакомиться с его api, это еще одна хорошо продуманная часть программного обеспечения. Я также написал статью в блоге, в которой описал некоторые из ее преимуществ.

Функция, которая интересует нас в этой статье, — это классы данных. Мы снова рассмотрим реализацию нашего класса Point. 😁

from pydantic.dataclasses import dataclass


@dataclass
class Point:
    x: int
    y: int


p1 = Point(1, 2)
print(p1)  # will print Point(x=1, y=2)

p2 = Point(1, 2)
print(p1 == p2)  # will print True

print(p1 == (1, 2))  # will print False

p = Point(1, 'foo')  # will raise a pydantic.ValidationError
Вход в полноэкранный режим Выход из полноэкранного режима

У нас есть все преимущества, которые мы имели с реализацией attrs, а написанный код стал еще меньше!

import dataclasses
import pydantic.dataclasses


@dataclasses.dataclass
class Point:
    x: int
    y: int


# no error raised
print(Point('foo', 2))

Point = pydantic.dataclasses.dataclass(Point)

# error raised
print(Point('foo', 2))
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь мы обернули обычный класс данных в pydantic, и у нас есть те же проверки и возможности!
Pydantic — фантастическая библиотека (да, я немного предвзят), и я могу только порекомендовать вам ознакомиться с ее документацией.

Вот и все по этому руководству, надеюсь, оно вам понравилось. Берегите себя и до встречи в следующий раз! 😁


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

Оцените статью
devanswers.ru
Добавить комментарий