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()
исследует класс, чтобы найтиfield
s. Переменная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.
field
s может опционально указывать значение по умолчанию, используя обычный синтаксис 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
Обратите внимание, что если поле аннотировано типом дескриптора, но ему не присвоен объект дескриптора в качестве значения по умолчанию, поле будет действовать как обычное поле.