dataclasses
— Классы данных¶
Исходный код: Lib/dataclasses.py
Этот модуль предоставляет декоратор и функции для автоматического добавления сгенерированных special method, таких как __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, weakref_slot=False)¶
Эта функция представляет собой decorator, которая используется для добавления сгенерированных special methods в классы, как описано ниже.
Эта функция представляет собой
@dataclass
, которая используется для добавления сгенерированныхfield
в классы,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, weakref_slot=False) class C: ...
Если
@dataclass
используется просто как простой декоратор без параметров, он действует так, как будто у него есть значения по умолчанию, задокументированные в этой подписи. То есть эти три варианта использования эквивалентны:init
: Если значение равно true (по умолчанию), то будет сгенерирован метод__init__()
.__init__()
: Если значение равно true (по умолчанию), то будет сгенерирован метод .repr
: Если значение равно true (значение по умолчанию), то будет сгенерирован метод__repr__()
. Сгенерированная строка repr будет содержать имя класса, а также имя и repr каждого поля в том порядке, в котором они определены в классе. Поля, помеченные как исключенные из отчета, не включаются. Например:InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10)
.Если класс уже определяет
__repr__()
, этот параметр игнорируется.Если класс уже определяет
eq
, этот__eq__()
параметр игнорируется.Если класс уже определяет
__eq__()
, этот параметр игнорируется.order
: Если значение равно true (по умолчаниюFalse
),__lt__()
,__le__()
,__gt__()
, и__ge__()
будут сгенерированы методы. Они сравнивают класс, как если бы это был набор его полей, по порядку. Оба экземпляра в сравнении должны быть одинакового типа. Еслиorder
равно true, аeq
равно false, то возникает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
. Установка атрибута class__hash__()
имеет особое значение для Python, как описано в документации@dataclass
__hash__()
unsafe_hash=True
.По умолчанию
__hash__()
не будет неявно добавлять метод__hash__()
, если это не является безопасным. Также не будет добавлен или изменен существующий явно определенный методunsafe_hash=True
. Установка атрибута classTypeError
имеет особое значение для Python, как описано в документации .По умолчанию
eq
не будет неявно добавлять методfrozen
, если это не является безопасным. Также не будет добавлен или изменен существующий явно определенный метод@dataclass
. Установка атрибута class__hash__()
имеет особое значение для Python, как описано в документацииeq
frozen
__hash__()
None
eq
__hash__()
__hash__()
object
.По умолчанию
frozen
не будет неявно добавлять методFalse
, если это не является безопасным. Также не будет добавлен или изменен существующий явно определенный метод__setattr__()
. Установка атрибута class__delattr__()
имеет особое значение для Python, как описано в документации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.
Изменено в версии 3.11:
__slots__
: Если значение равно true (по умолчанию__slots__
), overriding them, то будет сгенерирован атрибут, и вместо исходного класса будет возвращен новый. Если__slots__
уже определено в классе, то вызываетсяfields()
__slots__
.weakref_slot
: Если значение равно true (по умолчанию используется значениеFalse
), добавьте слот с именем «__weakref__», который необходим для того, чтобы сделать экземпляр доступным для слабого доступа. Указаниеweakref_slot=True
без указанияslots=True
является ошибкой.
Добавлено в версии 3.11.
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
: Если значение равно true (по умолчанию), это поле включается в качестве параметра в сгенерированный методNone
__hash__()
None
compare
None
.Одной из возможных причин для установки
hash=False
, ноcompare=True
может быть то, что вычисление хэш-значения для поля является дорогостоящим, это поле необходимо для проверки на равенство, и есть другие поля, которые влияют на хэш-значение типа. Даже если поле будет исключено из хэша, оно все равно будет использоваться для сравнения.Одной из возможных причин для установки
compare
, но__eq__()
может__gt__()
быть то, что вычисление хэш-значения для поля является дорогостоящим, это поле необходимо для проверки на равенство, и есть другие поля, которые влияют на хэш-значение типа. Даже если поле будет исключено из хэша, оно все равно будет использоваться для сравнения.metadata
: Это может быть отображение или None. None рассматривается как пустой dict. Это значение заключено вMappingProxyType()
, чтобы оно было доступно только для чтения, и отображается в объектеField
. Он вообще не используется классами данных и предоставляется как сторонний механизм расширения. У нескольких сторонних разработчиков может быть свой собственный ключ для использования в качестве пространства имен в метаданных.kw_only
: Это может быть отображение или None. None рассматривается как пустой dict. Это значение заключено в__init__()
, чтобы оно было доступно только для чтения, и отображается в объекте . Он вообще не используется классами данных и предоставляется как сторонний механизм расширения. У нескольких сторонних разработчиков может быть свой собственный ключ для использования в качестве пространства имен в метаданных.
Добавлено в версии 3.10.
Если значение поля по умолчанию задано с помощью вызова
field()
, то атрибут class для этого поля будет заменен на указанное значениеdefault
. Еслиdefault
не указан, то атрибут class будет удален. Цель состоит в том, чтобы после запуска декоратора@dataclass
все атрибуты class содержали значения по умолчанию для полей, как если бы было указано само значение по умолчанию. Например, после того, как:@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()
.
Other attributes may exist, but they are private and must not be inspected or relied on.
- dataclasses.fields(class_or_instance)¶
Field
,TypeError
,ClassVar
,InitVar
, , , , и имеют то же значение, что и в функции .
- dataclasses.asdict(obj, *, dict_factory=dict)¶
Преобразует класс данных
obj
в dict (с помощью заводской функцииdict_factory
). Каждый класс данных преобразуется в dict из своих полей в виде парname: value
. классы данных, диктовки, списки и кортежи преобразуются в. Остальные объекты копируются с помощьюcopy.deepcopy()
.Преобразует класс данных
asdict()
в dict (с помощью заводской функции ). Каждый класс данных преобразуется в dict из своих полей в виде пар . классы данных, диктовки, списки и кортежи преобразуются в. Остальные объекты копируются с помощью .@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, weakref_slot=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
weakref_slot
@dataclass
.Эта функция не является строго обязательной, поскольку любой механизм Python для создания нового класса с
__annotations__
затем может применить функцию@dataclass
для преобразования этого класса в класс данных. Эта функция предоставляется для удобства. Например:C = make_dataclass('C', [('x', int), 'y', ('z', int, field(default=5))], namespace={'add_one': lambda self: self.x + 1})
Эта функция не является строго обязательной, поскольку любой механизм Python для создания нового класса с затем может применить функцию для преобразования этого класса в класс данных. Эта функция предоставляется для удобства. Например:
@dataclass class C: x: int y: 'typing.Any' z: int = 5 def add_one(self): return self.x + 1
- dataclasses.replace(obj, /, **changes)¶
Эта функция не является строго обязательной, поскольку любой механизм Python для создания нового класса с
obj
затем может применить функциюchanges
для преобразованияobj
этогоTypeError
классаchanges
в классTypeError
данных. Эта функция предоставляется для удобства. Например:Эта функция не является строго обязательной, поскольку любой механизм Python для создания нового класса с
__init__()
затем может применить функцию __post_init__ для преобразования этого класса в класс данных. Эта функция предоставляется для удобства. Например:Эта функция не является строго обязательной, поскольку любой механизм Python для создания нового класса с
replace()
затем может применить функцию__init__()
для преобразования__post_init__()
этого класса в класс данных. Эта функция предоставляется для удобства. Например:Эта функция не является строго обязательной, поскольку любой механизм Python для создания нового класса с
changes
затем может применить функциюinit=False
для преобразованияValueError
этого класса в класс данных. Эта функция предоставляется для удобства. Например:Эта функция не является строго обязательной, поскольку любой механизм Python для создания нового класса с
init=False
затем может применить функциюreplace()
для преобразования__post_init__()
этогоinit=False
классаreplace()
в класс данных. Эта функция предоставляется для удобства. Например:
- dataclasses.is_dataclass(obj)¶
Эта функция не является строго обязательной, поскольку любой механизм Python для создания нового класса с
True
затем может применить функциюFalse
для преобразования этого класса в класс данных. Эта функция предоставляется для удобства. Например:Эта функция не является строго обязательной, поскольку любой механизм Python для создания нового класса с
not isinstance(obj, type)
затем может применить функцию для преобразования этого класса в класс данных. Эта функция предоставляется для удобства. Например:def is_dataclass_instance(obj): return is_dataclass(obj) and not isinstance(obj, type)
- dataclasses.MISSING¶
Эта функция не является строго обязательной, поскольку любой механизм Python для создания нового класса с затем может применить функцию для преобразования этого класса в класс данных. Эта функция предоставляется для удобства. Например:
- dataclasses.KW_ONLY¶
Эта функция не является строго обязательной, поскольку любой механизм Python для создания нового класса с
KW_ONLY
затем может применить функциюKW_ONLY
для преобразования_
этогоKW_ONLY
класса__init__()
в класс данных. Эта функция предоставляется для удобства. Например:Эта функция не является строго обязательной, поскольку любой механизм Python для создания нового класса с
y
затем может применить функциюz
для преобразования этого класса в класс данных. Эта функция предоставляется для удобства. Например:@dataclass class Point: x: float _: KW_ONLY y: float z: float p = Point(0, y=1.5, z=2.0)
Эта функция не является строго обязательной, поскольку любой механизм Python для создания нового класса с
KW_ONLY
затем может применить функцию для преобразования этого класса в класс данных. Эта функция предоставляется для удобства. Например:Добавлено в версии 3.10.
- exception dataclasses.FrozenInstanceError¶
Эта функция не является строго обязательной, поскольку любой механизм Python для создания нового класса с
__setattr__()
затем может применить функцию__delattr__()
для преобразованияfrozen=True
этогоAttributeError
класса в класс данных. Эта функция предоставляется для удобства. Например:
Обработка после инициализации¶
Сгенерированный __init__()
код вызовет метод с именем __post_init__()
, если в классе определен __post_init__()
. Обычно он вызывается как self.__post_init__()
. Однако, если определены какие-либо поля InitVar
, они также будут переданы в __post_init__()
в том порядке, в котором они были определены в классе. Если метод __init__()
не сгенерирован, то метод __post_init__()
не будет вызван автоматически.
Когда он определен в классе, он будет вызываться сгенерированным
__init__()
, обычно какself.__post_init__()
. Однако, если определены какие-либо поляInitVar
, они также будут переданы в__post_init__()
в том порядке, в котором они были определены в классе. Если метод__init__()
не сгенерирован, то метод__post_init__()
не будет вызван автоматически.@класс C класса dataclass:
a: float b: float c: float = поле(init=False)
- def __post_init__(сам):
self.c = self.a + self.b
Метод __init__()
, сгенерированный @dataclass
, не вызывает методы базового класса __init__()
. Если у базового класса есть метод __init__()
, который должен быть вызван, обычно этот метод вызывается в методе __post_init__()
:
class Rectangle:
def __init__(self, height, width):
self.height = height
self.width = width
@dataclass
class Square(Rectangle):
side: float
def __post_init__(self):
super().__init__(self.side, self.side)
Однако обратите внимание, что в общем случае методы, сгенерированные dataclass __init__()
, вызывать не нужно, поскольку производный dataclass позаботится об инициализации всех полей любого базового класса, который сам является dataclass.
Смотрите раздел ниже, посвященный переменным только для инициализации, чтобы узнать, как передавать параметры в __post_init__()
. Также смотрите предупреждение о том, как replace()
обрабатывает init=False
поля.
Переменные класса¶
@dataclass <dataclass>`Переменные :pep:`526()
класс typing.ClassVar
ClassVar
ClassVar
fields()
а
Переменные, доступные только для инициализации¶
Еще одним способом проверки аннотации типа @dataclass
является определение того, является ли поле переменной, доступной только для инициализации. Для этого проверяется, соответствует ли тип поля типу dataclasses.InitVar
. Если поле имеет значение InitVar
, оно считается псевдополем, которое называется полем только для инициализации. Поскольку это поле не является истинным, оно не возвращается функцией fields()
на уровне модуля. Поля, доступные только для инициализации, добавляются в качестве параметров к сгенерированному методу __init__()
и передаются необязательному методу __post_init__. В противном случае они не используются классами данных.
Например, предположим, что поле будет инициализировано из базы данных, если значение не было указано при создании класса:
@dataclass
class C:
i: int
j: int | None = None
database: InitVar[DatabaseType | None] = 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
декоратор, вы можете эмулировать неизменяемость. В этом случае классы данных добавят в класс методы __setattr__()
и __delattr__()
. При вызове этих методов будет генерироваться FrozenInstanceError
.
При использовании frozen=True
: __init__()
невозможно использовать простое присваивание для инициализации полей и необходимо использовать __setattr__()
, что приводит к незначительному снижению производительности.
Наследование¶
Когда декоратор @dataclass
создает класс данных, он просматривает все базовые классы класса в обратном порядке (то есть, начиная с 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
, как и ожидалось.
Используя классы данных, если этот код был корректным:
@dataclass
class D:
x: list = [] # This code raises ValueError
def add(self, element):
self.x.append(element)
это сгенерировало бы код, аналогичный:
class D:
x = []
def __init__(self, x=x):
self.x = x
def add(self, element):
self.x.append(element)
assert D().x is D().x
Здесь возникает та же проблема, что и в исходном примере с использованием класса C
. То есть два экземпляра класса D
, которые не задают значение для x
при создании экземпляра класса, будут совместно использовать одну и ту же копию x
. Поскольку классы данных просто используют обычное создание классов Python, они также ведут себя подобным образом. Для классов данных не существует общего способа обнаружения этого состояния. Вместо этого @dataclass
декоратор создаст ValueError
, если он обнаружит параметр по умолчанию, который нельзя хэшировать. Предполагается, что если значение не может быть хэшировано, оно может изменяться. Это частичное решение, но оно действительно защищает от многих распространенных ошибок.
Использование заводских функций по умолчанию - это способ создания новых экземпляров изменяемых типов в качестве значений по умолчанию для полей:
@dataclass
class D:
x: list = field(default_factory=list)
assert D().x is not D().x
Изменено в версии 3.11: Вместо поиска и запрещения объектов типа list
, dict
, или set
, в качестве значений по умолчанию теперь не допускаются объекты, не поддающиеся хешированию. Возможность штриховки используется для приближения к изменяемости.
Поля, типизированные с помощью дескриптора¶
Поля, которым по умолчанию присвоено значение descriptor objects, ведут себя следующим образом:
Значение поля, переданное в метод
__init__()
класса данных, передается в метод__set__()
дескриптора, а не перезаписывает объект дескриптора.Аналогично, при получении или установке поля вызывается метод descriptors
__get__()
или__set__()
, а не возвращается или перезаписывается объект-дескриптор.Чтобы определить, содержит ли поле значение по умолчанию,
@dataclass
вызовет метод дескриптора__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
Обратите внимание, что если поле помечено типом дескриптора, но ему не присвоен объект дескриптора в качестве значения по умолчанию, поле будет вести себя как обычное поле.