Python dataclass post init

Введение в Python Data Classes (часть 2)

Я Сергей Балакирев и мы продолжаем тему Data Classes – классы данных. На предыдущем занятии мы в целом научились объявлять такие классы с помощью декоратора dataclass и определять набор необходимых атрибутов:

@dataclass class ThingData: name: str weight: int price: float = 0 dims: list = field(default_factory=list)

Но давайте представим, что нам нужно в некотором классе, например, Vector3D при инициализации формировать вычисляемое свойство:

class Vector3D: def __init__(self, x: int, y: int, z: int): self.x = x self.y = y self.z = z self.length = (x * x + y * y + z * z) ** 0.5

Здесь локальный атрибут length вычисляется на основе параметров x, y, z. Как это можно сделать при объявлении Data Classes?

Метод __post_init__()

Вначале, очевидно, нужно прописать класс с тремя атрибутами следующим образом:

@dataclass class V3D: x: int y: int z: int

Что делать дальше, как определить локальный атрибут length внутри объекта класса V3D? Для этого существует следующая хитрость. Инициализаторы классов, сформированные с помощью декоратора dataclass, в конце своего вызова вызывают специальный метод __post_init__(). Именно в этом методе можно формировать любые вычисляемые свойства, например, так:

@dataclass class V3D: x: int y: int z: int def __post_init__(self): self.length = (self.x * self.x + self.y * self.y + self.z * self.z) ** 0.5

Если далее мы сформируем объект этого класса и выведем его в консоль:

Спрашивается, почему здесь не видно свойства length? В действительности, оно присутствует в объекте v и мы в этом легко можем убедиться:

Но тогда почему оно не выводится функцией repr()? Дело в том, что магический метод __repr__() выводит только те атрибуты, которые были указаны при объявлении класса. Все остальные, что создаются в процессе формирования объекта, не учитываются в методе __repr__(). Как тогда выйти из этой ситуации и указать, что локальный атрибут length также следует отображать? Очень просто! Давайте укажем этот атрибут при объявлении класса с небольшим уточнением:

@dataclass class V3D: x: int y: int z: int length: float = field(init=False) def __post_init__(self): self.length = (self.x * self.x + self.y * self.y + self.z * self.z) ** 0.5

Мы здесь воспользовались уже знакомой нам функцией field() и отметили, что атрибут length не следует использовать в качестве параметра инициализатора. Изящно, правда?! Теперь, при отображении объекта v класса, мы увидим и параметр length.

Функция field()

  • repr – булевое значение True/False указывает использовать ли атрибут в магическом методе __repr__() (по умолчанию True);
  • compare – булевое значение True/False указывает использовать ли атрибут при сравнении объектов (по умолчанию True);
  • default – значение по умолчанию (начальное значение).
@dataclass class V3D: x: int = field(repr=False) y: int z: int = field(compare=False) length: float = field(init=False, compare=False) def __post_init__(self): self.length = (self.x * self.x + self.y * self.y + self.z * self.z) ** 0.5
v = V3D(1, 2, 3) v2 = V3D(1, 2, 5) print(v) print(v == v2)

мы увидим только три атрибута: y, z, length и результат True, т.к. координаты x, y объектов v и v2 совпадают. С остальными параметрами функции field() можно познакомиться на странице официальной документации: https://docs.python.org/3/library/dataclasses.html

Объявление параметров типа InitVar

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

class Vector3D: def __init__(self, x: int, y: int, z: int, calc_len: bool = True): self.x = x self.y = y self.z = z self.length = (x * x + y * y + z * z) ** 0.5 if calc_len else 0

А как это сделать при объявлении Data Classes? Для определения параметров, участвующих в инициализации (таких, как calc_len) в модуле dataclasses есть специальный класс типа InitVar:

from dataclasses import dataclass, field, InitVar

Если при объявлении атрибут аннотируется этим классом, то он автоматически передается как параметр в метод __post_init__(), чтобы им можно было воспользоваться при формировании вычисляемых свойств:

@dataclass class V3D: x: int = field(repr=False) y: int z: int = field(compare=False) calc_len: InitVar[bool] = True length: float = field(init=False, compare=False, default=0) def __post_init__(self, calc_len: bool): if calc_len: self.length = (self.x * self.x + self.y * self.y + self.z * self.z) ** 0.5

Обратите внимание, я здесь для атрибута length добавил параметр default=0 в функции field(). То есть, начальное значение атрибута length равно нулю. Если параметр calc_len равен True, то в методе __post_init__() будет пересчитано и сформировано новое значение локального атрибута length.

Параметры декоратора dataclass

До сих пор мы с вами использовали декоратор dataclass с параметрами по умолчанию. Однако ему можно передавать различные аргументы и управлять процессом формирования итогового класса. Вот основные параметры, которые принимает декоратор dataclass.

Параметр Описание
init = [True | False] Принимает булево значение, по умолчанию True. Если значение True, то в классе объявляется инициализатор, иначе – не объявляется.
repr = [True | False] Принимает булево значение, по умолчанию True. Если значение True, то в классе объявляется магический метод __repr__(), иначе – не объявляется.
eq = [True | False] Принимает булево значение, по умолчанию True. Если значение True, то в классе объявляется магический метод __eq__(), иначе – не объявляется.
order = [True | False] Принимает булево значение, по умолчанию False. Если значение True, то в классе объявляются магические методы для операций сравнения ; >=, иначе – не объявляются.
unsafe_hash = [True | False] Влияет на формирование магического метода __hash__()
frozen = [True | False] Принимает булево значение, по умолчанию False. Если значение True, то атрибуты объектов класса становятся неизменными (можно только проинициализировать один раз в инициализаторе).
slots = [True | False] Принимает булево значение, по умолчанию False. Если значение True, то атрибуты объявляются в коллекции __slots__.

Существуют и другие параметры декоратора dataclass. Подробно о них можно почитать на странице официальной документации: https://docs.python.org/3/library/dataclasses.html Давайте последовательно рассмотрим основные из них. Первые параметры init, repr, eq я, думаю, понятны. Если в декоратор передать аргумент init=False:

то класс будет сформирован без собственного инициализатора (будет использован инициализатор базового класса). В результате у нас не получится создать объект с передачей значений аргументов:

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

запрещает формирование магического метода __repr__() внутри текущего класса. В результате, будет использован аналогичный метод базового класса. В этом легко убедиться, если создать объект и вывести его в консоль:

@dataclass(repr=False, eq=False)

Он запрещает формирование собственного магического метода __eq__() для сравнения объектов между собой на равенство. Теперь объекты сравниваются по их идентификаторам, и так как они разные, то при сравнении:

v = V3D(1, 2, 3, False) v2 = V3D(1, 2, 3) print(v == v2)

получаем значение False. Следующий параметр order может быть установлен в True только совместно с eq=True. Например, следующая строчка приведет к ошибке:

@dataclass(eq=False, order=True)

Поэтому нам нужно или убрать параметр eq (по умолчанию он True), либо явно прописать у него значение True:

@dataclass(eq=True, order=True)

Я, думаю, вы догадались почему? Операции сравнения на больше или равно, меньше или равно используют магический метод __eq__(). Поэтому он должен присутствовать. Итак, после включения параметра order, у нас появляется возможность сравнивать объекты класса на больше, меньше и больше или равно и меньше или равно:

@dataclass(eq=True, order=True) class V3D: x: int y: int z: int v = V3D(1, 2, 5) v2 = V3D(1, 2, 3) print(v  v2) # False print(v > v2) # True

Сравнение выполняется на уровне кортежей, содержащих значения атрибутов (x, y, z) в порядке их объявления в классе. В данном случае происходит последовательное сравнение сначала значений x между собой, затем, y и потом – z. Как только встречается пара, для которой можно вычислить значение True или False, проверка завершается. Фактически, в приведенном примере, сравниваются между собой только последние числа 5 и 3, остальные равны, поэтому операции < и >их пропускают. Если нам нужно исключить какие-либо атрибуты из операций сравнений, то, как я уже отмечал, для этого следует использовать функцию field() и в ней через параметр compare исключить соответствующее поле:

@dataclass(eq=True, order=True) class V3D: x: int = field(compare=False) y: int z: int

Теперь сравниваться будут объекты только по двум локальным атрибутам y и z. Здесь следует обратить внимание на то, что если в классе объявить какой-либо метод сравнения на больше, меньше или больше либо равно или меньше либо равно, то возникнет исключение TypeError:

@dataclass(order=True) class V3D: x: int = field(compare=False) y: int z: int def __lt__(self, other): return self.x  other.x and self.y  other.y

Последний параметр, который мы рассмотрим – frozen, позволяет «замораживать» значения атрибутов класса. Например:

@dataclass(frozen=True) class V3D: x: int y: int z: int v = V3D(1, 2, 3) print(v) v.x = 5

Источник

Читайте также:  Html connection time out
Оцените статью