Spec-Zone.ru › Python 3.12

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: Если значение истинно (по умолчанию), будет сгенерирован метод __init__().

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

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

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

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

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

  • order: Если значение истинно (по умолчанию 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__(). Установка атрибута класса __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, это означает, что хеширование будет основано на идентификаторе объекта).

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

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

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

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

  • slots: Если значение истинно (по умолчанию 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: Если значение истинно (по умолчанию 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: Если значение истинно (по умолчанию), это поле включается в параметры сгенерированного метода __init__().
  • repr: Если значение истинно (по умолчанию), это поле включается в строку, возвращаемую сгенерированным методом __repr__().
  • hash: Значением может быть bool или None. Если значение истинно, поле включается в сгенерированный метод __hash__(). Если значение ложно, поле исключается из сгенерированного метода __hash__(). Если указано None (значение по умолчанию), используется значение compare: обычно это ожидаемое поведение, поскольку поле следует включать в хеш, если оно используется при сравнении. Не рекомендуется устанавливать это значение во что-либо, кроме None.

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

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

Добавлено в версии 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().

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

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)}

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

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))

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

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() (или с похожим именем), который обрабатывает копирование экземпляров.

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 проверяет аннотацию типа, — это определение того, является ли поле переменной, предназначенной только для инициализации. Для этого проверяется, имеет ли тип поля тип 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__() не может использовать простое присваивание для инициализации полей и должен использовать 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–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.12/library/dataclasses.html

Spec-Zone.ru

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