Spec-Zone.ru › Python 3.13

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

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

Этот модуль предоставляет декоратор и функции для автоматического добавления в определённые пользователем классы сгенерированных специальных методов, таких как __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)

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

Декоратор @dataclass просматривает класс в поисках field. field определяется как переменная класса с аннотацией типа. За исключением двух описанных ниже случаев, ничто в @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__(), этот параметр игнорируется.

  • 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 имеет значение true, а eq — false, возникает исключение ValueError.

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

  • unsafe_hash: Если значение равно true, принудительно заставляет dataclasses создать метод __hash__(), даже если это может быть небезопасно. В противном случае метод __hash__() создаётся в зависимости от значений eq и frozen. Значение по умолчанию — False.

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

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

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

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

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

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

    Если в классе определён __setattr__() или __delattr__(), а frozen имеет значение true, возникает исключение TypeError.

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

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

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

    Поля, доступные только по ключевому слову, не включаются в __match_args__.

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

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

Предупреждение

Вызов super() без аргументов в классах данных, использующих slots=True, приведёт к возникновению следующего исключения: TypeError: super(type, obj): obj must be an instance or subtype of type. В качестве обходного решения можно использовать super() с двумя аргументами. Полное описание см. в gh-90562.

Предупреждение

Передача параметров базовому классу __init_subclass__() при использовании slots=True приведёт к возникновению исключения TypeError. В качестве обходного решения используйте __init_subclass__ без параметров или значения по умолчанию. Полное описание см. в gh-91126.

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

Изменено в версии 3.11: Если имя поля уже содержится в __slots__ базового класса, оно не будет включено в сгенерированный __slots__, чтобы предотвратить переопределение этих полей. Поэтому не используйте __slots__ для получения имён полей класса данных. Вместо этого используйте fields(). Чтобы можно было определить унаследованные слоты, __slots__ базового класса может быть любым итерируемым объектом, но не итератором.

  • weakref_slot: Если значение равно true (по умолчанию False), добавляется слот с именем «__weakref__», необходимый, чтобы экземпляр можно было weakref-able. Указывать weakref_slot=True без slots=True нельзя.

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

Для field можно указать значение по умолчанию, используя обычный синтаксис 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: Может быть булевым значением или None. Если значение равно true, это поле включается в сгенерированный метод __hash__(). Если значение равно false, поле исключается из сгенерированного метода __hash__(). Если значение равно None (по умолчанию), используется значение compare: обычно это ожидаемое поведение, поскольку поле следует включать в хеш, если оно используется при сравнении. Не рекомендуется устанавливать для этого параметра значение, отличное от None.

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

  • compare: Если значение равно true (по умолчанию), это поле включается в сгенерированные методы проверки равенства и сравнения (__eq__(), __gt__() и другие).
  • metadata: Может быть отображением или None. Значение None трактуется как пустой словарь. Это значение оборачивается в MappingProxyType(), чтобы сделать его доступным только для чтения, и предоставляется через объект Field. Классы данных никак его не используют; оно предназначено для расширения сторонними разработчиками. Каждая сторонняя библиотека может использовать собственный ключ в качестве пространства имён в метаданных.
  • kw_only: Если значение равно true, это поле помечается как доступное только по ключевому слову. Этот параметр используется при вычислении параметров сгенерированного метода __init__().

    Поля, доступные только по ключевому слову, также не включаются в __match_args__.

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

Если значение поля по умолчанию задаётся вызовом field(), атрибут класса для этого поля будет заменён указанным значением default. Если default не задан, атрибут класса будет удалён. Предполагается, что после выполнения декоратора @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().

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

class dataclasses.InitVar

Аннотации типов InitVar[T] описывают переменные, являющиеся только инициализационными. Поля с аннотацией InitVar считаются псевдополями, поэтому они не возвращаются функцией fields() и никак не используются, кроме как в качестве параметров __init__() и необязательного __post_init__().

dataclasses.fields(class_or_instance)

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

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

Преобразует класс данных obj в словарь (используя фабричную функцию 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}]}

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

{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, module=None)

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

Если задан module, атрибут __module__ класса данных устанавливается в это значение. По умолчанию ему присваивается имя модуля вызывающего кода.

Эта функция не является строго необходимой, поскольку любой механизм 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() (или метод с похожим именем) для копирования экземпляров.

Экземпляры классов данных также поддерживаются универсальной функцией copy.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.

Обработка после инициализации

dataclasses.__post_init__()

Если этот метод определён в классе, он будет вызван сгенерированным методом __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__():

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)

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

О способах передачи параметров в __post_init__() см. раздел ниже о переменных, используемых только при инициализации. Также см. предупреждение о том, как replace() обрабатывает поля init=False.

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

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

Переменные, используемые только при инициализации

Ещё одно место, где @dataclass проверяет аннотацию типа, — это определение того, является ли поле переменной, используемой только при инициализации. Для этого проверяется, имеет ли тип поля тип 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__() не может использовать простое присваивание для инициализации полей и должен использовать 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, как и следовало ожидать.

Если бы этот код с классами данных был допустим:

@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. Нехешируемость используется как приближённый признак изменяемости.

Поля с типом-дескриптором

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

  • Значение поля, переданное методу __init__() класса данных, передаётся методу __set__() дескриптора, а не заменяет объект-дескриптор.
  • Аналогично, при получении или задании значения поля вызывается метод __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

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

© 2001–2025 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/library/dataclasses.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API