dataclasses — Классы данных

Исходный код: Lib/dataclasses.py.


Этот модуль предоставляет декоратор и функции для автоматического добавления сгенерированных special methods, таких как __init__() и __repr__(), в классы, определяемые пользователем. Первоначально он был описан в PEP 557.

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

from dataclasses import dataclass

@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

добавит, помимо прочего, __init__(), который выглядит как:

def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0):
    self.name = name
    self.unit_price = unit_price
    self.quantity_on_hand = quantity_on_hand

Обратите внимание, что этот метод автоматически добавляется в класс: он не указан напрямую в определении InventoryItem, показанном выше.

Добавлено в версии 3.7.

Содержание модуля

@dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False)

Эта функция представляет собой decorator, которая используется для добавления сгенерированных special methodк классам, как описано ниже.

Декоратор dataclass() исследует класс, чтобы найти fields. Переменная field определяется как переменная класса, которая имеет аннотацию type annotation. За двумя исключениями, описанными ниже, ничто в dataclass() не рассматривает тип, указанный в аннотации переменной.

Порядок полей во всех сгенерированных методах соответствует порядку их появления в определении класса.

Декоратор dataclass() добавляет в класс различные методы «dunder», описанные ниже. Если какой-либо из добавленных методов уже существует в классе, поведение зависит от параметра, как описано ниже. Декоратор возвращает тот же класс, для которого он был вызван; новый класс не создается.

Если dataclass() используется просто как простой декоратор без параметров, то он действует так, как будто имеет значения по умолчанию, документированные в этой сигнатуре. То есть, эти три варианта использования dataclass() эквивалентны:

@dataclass
class C:
    ...

@dataclass()
class C:
    ...

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False)
class C:
   ...

Параметрами для dataclass() являются:

  • init: Если true (по умолчанию), будет сгенерирован метод __init__().

    Если класс уже определяет __init__(), этот параметр игнорируется.

  • repr: Если true (по умолчанию), будет сгенерирован метод __repr__(). Сгенерированная строка repr будет содержать имя класса, имя и repr каждого поля, в том порядке, в котором они определены в классе. Поля, которые помечены как исключенные из repr, не будут включены. Например: InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10).

    Если класс уже определяет __repr__(), этот параметр игнорируется.

  • eq: Если true (по умолчанию), будет сгенерирован метод __eq__(). Этот метод сравнивает класс, как если бы он был кортежем его полей, по порядку. Оба экземпляра в сравнении должны быть одинакового типа.

    Если класс уже определяет __eq__(), этот параметр игнорируется.

  • order: Если true (по умолчанию False), будут сгенерированы методы __lt__(), __le__(), __gt__() и __ge__(). Они сравнивают класс, как если бы это был кортеж его полей, по порядку. Оба экземпляра в сравнении должны быть одинакового типа. Если order истинно, а eq ложно, то возникает ошибка ValueError.

    Если класс уже определяет любой из __lt__(), __le__(), __gt__() или __ge__(), то возникает вопрос TypeError.

  • unsafe_hash: Если False (по умолчанию), генерируется метод __hash__() в соответствии с тем, как установлены eq и frozen.

    __hash__() используется встроенными hash(), а также при добавлении объектов в хешированные коллекции, такие как словари и множества. Наличие __hash__() подразумевает, что экземпляры класса неизменяемы. Неизменяемость - это сложное свойство, которое зависит от намерений программиста, существования и поведения __eq__() и значений флагов eq и frozen в декораторе dataclass().

    По умолчанию dataclass() не будет неявно добавлять метод __hash__(), если это небезопасно. Он также не будет добавлять или изменять существующий явно определенный метод __hash__(). Установка атрибута class __hash__ = None имеет специфическое значение для Python, как описано в документации __hash__().

    Если __hash__() явно не определен, или если он установлен в None, то dataclass() может добавить неявный метод __hash__(). Хотя это не рекомендуется, вы можете заставить dataclass() создать метод __hash__() с помощью unsafe_hash=True. Это может произойти, если ваш класс логически неизменяем, но, тем не менее, может быть изменен. Это особый случай использования, и его следует тщательно обдумать.

    Вот правила, регулирующие неявное создание метода __hash__(). Обратите внимание, что вы не можете одновременно иметь явный метод __hash__() в вашем классе данных и задать unsafe_hash=True; это приведет к появлению TypeError.

    Если eq и frozen оба истинны, по умолчанию dataclass() сгенерирует для вас метод __hash__(). Если eq истинно, а frozen ложно, __hash__() будет установлен в None, помечая его как нехешируемый (что так и есть, поскольку он мутабелен). Если eq равно false, __hash__() будет оставлено нетронутым, то есть будет использоваться метод __hash__() суперкласса (если суперкласс object, это означает, что он вернется к хэшированию на основе id).

  • frozen: Если true (по умолчанию False), присвоение полям будет генерировать исключение. Это эмулирует замороженные экземпляры, доступные только для чтения. Если в классе определены __setattr__() или __delattr__(), то возникает TypeError. См. обсуждение ниже.

  • match_args: Если true (по умолчанию True), то кортеж __match_args__ будет создан из списка параметров сгенерированного метода __init__() (даже если __init__() не сгенерирован, см. выше). Если false, или если __match_args__ уже определен в классе, то __match_args__ не будет сгенерирован.

Добавлено в версии 3.10.

  • kw_only: Если true (значение по умолчанию False), то все поля будут помечены как предназначенные только для ключевых слов. Если поле помечено как предназначенное только для ключевых слов, то единственное влияние заключается в том, что параметр __init__(), сгенерированный из поля, предназначенного только для ключевых слов, должен быть указан ключевым словом при вызове __init__(). Это не влияет ни на какие другие аспекты классов данных. Подробности см. в глоссарии parameter. Также см. раздел KW_ONLY.

Добавлено в версии 3.10.

  • slots: Если true (по умолчанию False), то будет сгенерирован атрибут __slots__ и возвращен новый класс вместо исходного. Если __slots__ уже определен в классе, то будет выдано сообщение TypeError.

Добавлено в версии 3.10.

fields может опционально указывать значение по умолчанию, используя обычный синтаксис Python:

@dataclass
class C:
    a: int       # 'a' has no default value
    b: int = 0   # assign a default value for 'b'

В этом примере оба метода a и b будут включены в добавленный метод __init__(), который будет определен как:

def __init__(self, a: int, b: int = 0):

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

dataclasses.field(*, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING)

Для обычных и простых случаев использования не требуется никакой другой функциональности. Однако есть некоторые функции класса данных, которые требуют дополнительной информации для каждого поля. Чтобы удовлетворить потребность в дополнительной информации, вы можете заменить значение поля по умолчанию вызовом функции field(). Например:

@dataclass
class C:
    mylist: list[int] = field(default_factory=list)

c = C()
c.mylist += [1, 2, 3]

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

Параметрами для field() являются:

  • default: Если указано, это будет значение по умолчанию для данного поля. Это необходимо, поскольку вызов field() сам по себе заменяет обычную позицию значения по умолчанию.

  • default_factory: Если указано, то это должен быть вызываемый элемент с нулевым аргументом, который будет вызван, когда для этого поля потребуется значение по умолчанию. Помимо прочего, это может использоваться для указания полей с изменяемыми значениями по умолчанию, о чем будет сказано ниже. Ошибкой является указание как default, так и default_factory.

  • init: Если true (по умолчанию), это поле включается в качестве параметра в сгенерированный метод __init__().

  • repr: Если true (по умолчанию), то это поле включается в строку, возвращаемую сгенерированным методом __repr__().

  • hash: Это может быть bool или None. Если true, то это поле включается в генерируемый метод __hash__(). Если None (значение по умолчанию), используется значение compare: обычно это ожидаемое поведение. Поле должно учитываться в хэше, если оно используется для сравнений. Не рекомендуется устанавливать это значение в любое другое значение, кроме None.

    Одна из возможных причин установить hash=False, но compare=True - если для какого-либо поля дорого вычислять хэш-значение, это поле необходимо для проверки равенства, и есть другие поля, которые вносят вклад в хэш-значение типа. Даже если поле исключено из хэша, оно все равно будет использоваться для сравнений.

  • compare: Если true (по умолчанию), то это поле включается в генерируемые методы равенства и сравнения (__eq__(), __gt__() и др.).

  • metadata: Это может быть отображение или None. None рассматривается как пустой dict. Это значение оборачивается в MappingProxyType(), чтобы сделать его доступным только для чтения, и раскрывается на объекте Field. Оно вообще не используется классами данных и предоставляется в качестве механизма расширения сторонними разработчиками. Несколько сторонних объектов могут иметь каждый свой ключ, чтобы использовать его в качестве пространства имен в метаданных.

  • kw_only: Если true, то это поле будет помечено как предназначенное только для ключевых слов. Это используется при вычислении параметров сгенерированного метода __init__().

Добавлено в версии 3.10.

Если значение поля по умолчанию задано вызовом field(), то атрибут class для этого поля будет заменен указанным значением default. Если значение default не указано, то атрибут class будет удален. Смысл в том, что после выполнения декоратора dataclass() все атрибуты класса будут содержать значения по умолчанию для полей, как если бы было указано само значение по умолчанию. Например, после:

@dataclass
class C:
    x: int
    y: int = field(repr=False)
    z: int = field(repr=False, default=10)
    t: int = 20

Атрибут класса C.z будет 10, атрибут класса C.t будет 20, а атрибуты класса C.x и C.y не будут установлены.

class dataclasses.Field

Объекты Field описывают каждое определенное поле. Эти объекты создаются внутри модуля и возвращаются методом fields() на уровне модуля (см. ниже). Пользователи никогда не должны создавать объект Field напрямую. Его документированными атрибутами являются:

  • name: Имя поля.

  • type: Тип поля.

  • default, default_factory, init, repr, hash, compare, metadata и kw_only имеют тот же смысл и значения, что и в функции field().

Другие атрибуты могут существовать, но они являются частными, их нельзя проверять или полагаться на них.

dataclasses.fields(class_or_instance)

Возвращает кортеж объектов Field, определяющих поля для данного класса данных. Принимает либо класс данных, либо экземпляр класса данных. Вызывает сообщение TypeError, если не передан класс данных или его экземпляр. Не возвращает псевдополя, которые являются ClassVar или InitVar.

dataclasses.asdict(obj, *, dict_factory=dict)

Преобразует класс данных obj в dict (с помощью фабричной функции dict_factory). Каждый класс данных преобразуется в дикт его полей в виде пар name: value. Классы данных, дикты, списки и кортежи подвергаются рекурсии. Другие объекты копируются с помощью copy.deepcopy().

Пример использования asdict() на вложенных классах данных:

@dataclass
class Point:
     x: int
     y: int

@dataclass
class C:
     mylist: list[Point]

p = Point(10, 20)
assert asdict(p) == {'x': 10, 'y': 20}

c = C([Point(0, 0), Point(10, 4)])
assert asdict(c) == {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}

Для создания неглубокой копии можно использовать следующее обходное решение:

dict((field.name, getattr(obj, field.name)) for field in fields(obj))

asdict() выдает TypeError, если obj не является экземпляром класса данных.

dataclasses.astuple(obj, *, tuple_factory=tuple)

Преобразует класс данных obj в кортеж (с помощью фабричной функции tuple_factory). Каждый класс данных преобразуется в кортеж значений его полей. Классы данных, массивы, списки и кортежи рекурсируются. Другие объекты копируются с помощью функции copy.deepcopy().

Продолжая предыдущий пример:

assert astuple(p) == (10, 20)
assert astuple(c) == ([(0, 0), (10, 4)],)

Для создания неглубокой копии можно использовать следующее обходное решение:

tuple(getattr(obj, field.name) for field in dataclasses.fields(obj))

astuple() выдает TypeError, если obj не является экземпляром класса данных.

dataclasses.make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False)

Создает новый класс данных с именем cls_name, полями, определенными в fields, базовыми классами, указанными в bases, и инициализированным пространством имен, указанным в namespace. fields - это итерабельность, элементами которой являются либо name, либо (name, type), либо (name, type, Field). Если задано только name, typing.Any используется для type. Значения init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only и slots имеют тот же смысл, что и в dataclass().

Эта функция не является строго необходимой, поскольку любой механизм Python для создания нового класса с помощью __annotations__ может затем применить функцию dataclass() для преобразования этого класса в класс данных. Эта функция предоставляется в качестве удобства. Например:

C = make_dataclass('C',
                   [('x', int),
                     'y',
                    ('z', int, field(default=5))],
                   namespace={'add_one': lambda self: self.x + 1})

Это эквивалентно:

@dataclass
class C:
    x: int
    y: 'typing.Any'
    z: int = 5

    def add_one(self):
        return self.x + 1
dataclasses.replace(obj, /, **changes)

Создает новый объект того же типа, что и obj, заменяя поля значениями из changes. Если obj не является классом данных, возникает ошибка TypeError. Если значения в changes не определяют поля, возникает ошибка TypeError.

Вновь возвращаемый объект создается вызовом метода __init__() класса данных. Это гарантирует, что метод __post_init__(), если он присутствует, также будет вызван.

Только начальные переменные без значений по умолчанию, если таковые существуют, должны быть указаны при вызове replace(), чтобы их можно было передать в __init__() и __post_init__().

Ошибкой является то, что changes содержит любые поля, которые определены как имеющие init=False. В этом случае будет выдано предупреждение ValueError.

Будьте предупреждены о том, как работают поля init=False во время вызова replace(). Они не копируются из исходного объекта, а инициализируются в __post_init__(), если они вообще инициализируются. Ожидается, что поля init=False будут использоваться редко и осмотрительно. Если они используются, то целесообразно иметь альтернативные конструкторы класса или, возможно, пользовательский метод replace() (или с аналогичным названием), который обрабатывает копирование экземпляра.

dataclasses.is_dataclass(obj)

Возвращает True, если его параметр является классом данных или его экземпляром, в противном случае возвращает False.

Если вам нужно знать, является ли класс экземпляром класса данных (а не самим классом данных), то добавьте дополнительную проверку на not isinstance(obj, type):

def is_dataclass_instance(obj):
    return is_dataclass(obj) and not isinstance(obj, type)
dataclasses.MISSING

Значение, сигнализирующее об отсутствии значения по умолчанию или default_factory.

dataclasses.KW_ONLY

Сентинельное значение, используемое в качестве аннотации типа. Любые поля после псевдополя с типом KW_ONLY помечаются как поля только для ключевого слова. Обратите внимание, что псевдополе с типом KW_ONLY в противном случае полностью игнорируется. Это относится и к имени такого поля. По соглашению, для поля _ используется имя KW_ONLY. Поля только для ключевых слов обозначают __init__() параметры, которые должны быть указаны как ключевые слова при инстанцировании класса.

В этом примере поля y и z будут помечены как поля только для ключевых слов:

@dataclass
class Point:
  x: float
  _: KW_ONLY
  y: float
  z: float

p = Point(0, y=1.5, z=2.0)

Ошибкой является указание в одном классе данных более одного поля, тип которого KW_ONLY.

Добавлено в версии 3.10.

exception dataclasses.FrozenInstanceError

Возникает, когда неявно определенный __setattr__() или __delattr__() вызывается на классе данных, который был определен с помощью frozen=True. Он является подклассом AttributeError.

Обработка после запуска

Сгенерированный код __init__() вызовет метод с именем __post_init__(), если для класса определено __post_init__(). Обычно он будет вызываться как self.__post_init__(). Однако, если определены какие-либо поля InitVar, они также будут переданы в __post_init__() в том порядке, в котором они были определены в классе. Если метод __init__() не создан, то __post_init__() автоматически вызываться не будет.

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

@dataclass
class C:
    a: float
    b: float
    c: float = field(init=False)

    def __post_init__(self):
        self.c = self.a + self.b

Метод __init__(), генерируемый dataclass(), не вызывает методы базового класса __init__(). Если базовый класс имеет метод __init__(), который должен быть вызван, обычно этот метод вызывается в методе __post_init__():

@dataclass
class Rectangle:
    height: float
    width: float

@dataclass
class Square(Rectangle):
    side: float

    def __post_init__(self):
        super().__init__(self.side, self.side)

Заметим, однако, что в общем случае методы __init__(), порожденные классами данных, вызывать не нужно, поскольку производный класс данных позаботится об инициализации всех полей любого базового класса, который сам является классом данных.

Способы передачи параметров в __post_init__() см. в разделе ниже, посвященном переменным init-only. Также смотрите предупреждение о том, как replace() обрабатывает поля init=False.

Переменные класса

Одно из двух мест, где dataclass() действительно проверяет тип поля, - это определение того, является ли поле переменной класса, как определено в PEP 526. Для этого проверяется, является ли тип поля typing.ClassVar. Если поле является ClassVar, оно исключается из рассмотрения как поле и игнорируется механизмами классов данных. Такие псевдополя ClassVar не возвращаются функцией fields() на уровне модуля.

Только начальные переменные

Другое место, где dataclass() проверяет аннотацию типа, - это определение того, является ли поле переменной init-only. Для этого необходимо проверить, имеет ли поле тип dataclasses.InitVar. Если поле имеет тип InitVar, то оно считается псевдополем, называемым init-only field. Поскольку оно не является истинным полем, оно не возвращается функцией fields() на уровне модуля. Поля только для инициализации добавляются в качестве параметров в генерируемый метод __init__() и передаются в необязательный метод __post_init__(). В других случаях они не используются классами данных.

Например, предположим, что поле будет инициализировано из базы данных, если значение не указано при создании класса:

@dataclass
class C:
    i: int
    j: int = None
    database: InitVar[DatabaseType] = None

    def __post_init__(self, database):
        if self.j is None and database is not None:
            self.j = database.lookup('j')

c = C(10, database=my_database)

В этом случае fields() будет возвращать объекты Field для i и j, но не для database.

Замороженные экземпляры

Невозможно создать действительно неизменяемые объекты Python. Однако, передавая frozen=True в декоратор dataclass(), можно эмулировать неизменяемость. В этом случае dataclasses добавит в класс методы __setattr__() и __delattr__(). При вызове этих методов будет возникать ошибка FrozenInstanceError.

Существует небольшое снижение производительности при использовании frozen=True: __init__() не может использовать простое присваивание для инициализации полей, и должен использовать object.__setattr__().

Наследование

Когда класс данных создается декоратором dataclass(), он просматривает все базовые классы класса в обратном MRO (то есть, начиная с object) и для каждого класса данных, который он находит, добавляет поля из этого базового класса в упорядоченное отображение полей. После того как все поля базового класса добавлены, он добавляет свои собственные поля в упорядоченное отображение. Все созданные методы будут использовать это объединенное, вычисленное упорядоченное отображение полей. Поскольку поля располагаются в порядке вставки, производные классы переопределяют базовые классы. Пример:

@dataclass
class Base:
    x: Any = 15.0
    y: int = 0

@dataclass
class C(Base):
    z: int = 10
    x: int = 15

Конечный список полей имеет следующий порядок: x, y, z. Конечным типом x является int, как указано в классе C.

Сгенерированный метод __init__() для C будет выглядеть так:

def __init__(self, x: int = 15, y: int = 0, z: int = 10):

Переупорядочивание параметров только для ключевых слов в __init__()

После вычисления параметров, необходимых для __init__(), все параметры, содержащие только ключевые слова, перемещаются после всех обычных (не содержащих ключевые слова) параметров. Это требование реализации параметров только для ключевых слов в Python: они должны идти после параметров без ключевых слов.

В этом примере Base.y, Base.w и D.t являются полями только для ключевых слов, а Base.x и D.z - обычные поля:

@dataclass
class Base:
    x: Any = 15.0
    _: KW_ONLY
    y: int = 0
    w: int = 1

@dataclass
class D(Base):
    z: int = 10
    t: int = field(kw_only=True, default=0)

Сгенерированный метод __init__() для D будет выглядеть так:

def __init__(self, x: Any = 15.0, z: int = 10, *, y: int = 0, w: int = 1, t: int = 0):

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

В переупорядоченном списке параметров __init__() сохраняется относительное упорядочивание только ключевых слов.

Заводские функции по умолчанию

Если field() указывает на default_factory, он вызывается с нулевыми аргументами, когда требуется значение по умолчанию для поля. Например, чтобы создать новый экземпляр списка, используйте:

mylist: list = field(default_factory=list)

Если поле исключено из __init__() (с помощью init=False) и в поле также указано default_factory, то из сгенерированной функции __init__() всегда будет вызываться фабричная функция по умолчанию. Это происходит потому, что нет другого способа придать полю начальное значение.

Изменяемые значения по умолчанию

Python хранит значения переменных-членов по умолчанию в атрибутах класса. Рассмотрим этот пример без использования классов данных:

class C:
    x = []
    def add(self, element):
        self.x.append(element)

o1 = C()
o2 = C()
o1.add(1)
o2.add(2)
assert o1.x == [1, 2]
assert o1.x is o2.x

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

Используя dataclasses, если этот код был действителен:

@dataclass
class D:
    x: List = []
    def add(self, element):
        self.x += element

он будет генерировать код, подобный:

class D:
    x = []
    def __init__(self, x=x):
        self.x = x
    def add(self, element):
        self.x += element

assert D().x is D().x

Здесь возникает та же проблема, что и в исходном примере с использованием класса C. То есть, два экземпляра класса D, которые не указывают значение для x при создании экземпляра класса, будут иметь одну и ту же копию x. Поскольку классы данных просто используют обычное создание класса в Python, они также разделяют это поведение. Для классов данных не существует общего способа обнаружения этого условия. Вместо этого декоратор dataclass() будет вызывать ошибку TypeError, если обнаружит параметр по умолчанию типа list, dict или set. Это частичное решение, но оно защищает от многих распространенных ошибок.

Использование фабричных функций по умолчанию - это способ создания новых экземпляров изменяемых типов в качестве значений по умолчанию для полей:

@dataclass
class D:
    x: list = field(default_factory=list)

assert D().x is not D().x

Поля, типизированные для дескрипторов

Поля, которым присвоено значение по умолчанию descriptor objects, имеют следующие особенности поведения:

  • Значение поля, переданное в метод __init__ класса данных, передается в метод __set__ дескриптора, а не перезаписывает объект дескриптора.

  • Аналогично, при получении или установке поля вызывается метод дескриптора __get__ или __set__, а не возвращается или перезаписывается объект дескриптора.

  • Чтобы определить, содержит ли поле значение по умолчанию, dataclasses вызовет метод дескриптора __get__, используя его форму доступа к классу (т.е. descriptor.__get__(obj=None, type=cls). Если в этом случае дескриптор вернет значение, оно будет использовано в качестве значения по умолчанию для поля. С другой стороны, если в этой ситуации дескриптор выдает AttributeError, то для поля не будет предоставлено значение по умолчанию.

class IntConversionDescriptor:
  def __init__(self, *, default):
    self._default = default

  def __set_name__(self, owner, name):
    self._name = "_" + name

  def __get__(self, obj, type):
    if obj is None:
      return self._default

    return getattr(obj, self._name, self._default)

  def __set__(self, obj, value):
    setattr(obj, self._name, int(value))

@dataclass
class InventoryItem:
  quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100)

i = InventoryItem()
print(i.quantity_on_hand)   # 100
i.quantity_on_hand = 2.5    # calls __set__ with 2.5
print(i.quantity_on_hand)   # 2

Обратите внимание, что если поле аннотировано типом дескриптора, но ему не присвоен объект дескриптора в качестве значения по умолчанию, поле будет действовать как обычное поле.

Вернуться на верх