Spec-Zone.ru › Python 3.14

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__(), этот параметр игнорируется.

    Изменено в версии 3.13: Теперь сгенерированный метод __eq__ сравнивает каждое поле отдельно (например, self.a == other.a and self.b == other.b), а не сравнивает кортежи полей, как в предыдущих версиях.

    Это изменение ускоряет сравнение, но может изменить результаты в случаях, когда атрибуты равны по идентичности, но не по значению (например, float('nan')).

    В Python 3.12 и более ранних версиях сравнение выполнялось путём создания кортежей полей и их сравнения (например, (self.a, self.b) == (other.a, other.b)).

  • 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.

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

Передача параметров в __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, doc=None)

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

  • doc: необязательная строка документации для этого поля.

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

Если значение поля по умолчанию задано вызовом 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, decorator=dataclass)

Создаёт новый класс данных с именем 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__ класса данных получает это значение. По умолчанию ему присваивается имя модуля вызывающего кода.

Параметр decorator — это вызываемый объект, который будет использоваться для создания класса данных. Он должен принимать объект класса в качестве первого аргумента и те же именованные аргументы, что и @dataclass. По умолчанию используется функция @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

Добавлено в версии 3.14: Добавлен параметр decorator.

dataclasses.replace(obj, /, **changes)

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

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

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

Поля, определённые как init=False, не должны содержаться в changes. В этом случае возникает исключение 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

Значение-маркер, обозначающее отсутствие значения по умолчанию или фабрики значений по умолчанию.

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)

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

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

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

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

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

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

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

@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, можно имитировать неизменяемость. В этом случае dataclass добавит в класс методы __setattr__() и __delattr__(). При вызове эти методы возбуждают исключение FrozenInstanceError.

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

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

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

@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 хранит значения переменных-членов по умолчанию в атрибутах класса. Рассмотрим следующий пример без использования dataclass:

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 был допустим:

@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. Поскольку dataclass используют обычное создание классов в Python, им присуще такое же поведение. Не существует общего способа, позволяющего Data Classes обнаружить эту ситуацию. Вместо этого декоратор @dataclass возбуждает исключение ValueError, если обнаруживает нехешируемый параметр по умолчанию. Предполагается, что нехешируемое значение является изменяемым. Это решение не охватывает все случаи, но позволяет предотвратить многие распространённые ошибки.

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

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

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

Изменено в версии 3.11: Вместо поиска и запрета объектов типов list, dict или set теперь запрещены нехешируемые объекты в качестве значений по умолчанию. Нехешируемость используется как приближённый признак изменяемости.

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

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

  • Значение поля, передаваемое методу __init__() dataclass, передаётся методу __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 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.14/library/dataclasses.html

Spec-Zone.ru

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