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

Источник: Lib/dataclasses.py


Этот модуль предоставляет декоратор и функции для автоматического добавления сгенерированных special methods, таких как __init__() и __repr__(), в классы, определяемые пользователем. Первоначально он был описан в PEP 557.

Переменные-члены, которые будут использоваться в этих сгенерированных методах, определяются с помощью аннотаций типа PEP 526. Например, этот код:

from dataclasses import dataclass

@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

добавит, помимо всего прочего, __init__(), который выглядит так:

def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0):
    self.name = name
    self.unit_price = unit_price
    self.quantity_on_hand = quantity_on_hand

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

Added in version 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 добавит в класс различные «дундерские» методы, описанные ниже. Если какой-либо из добавленных методов уже существует в классе, поведение зависит от параметра, как описано ниже. Декоратор возвращает тот же класс, на котором он был вызван; новый класс не создается.

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

  • порядок: Если значение равно true (по умолчанию False), будут сгенерированы методы __lt__(), __le__(), __gt__() и __ge__(). Они сравнивают класс, как если бы это был кортеж его полей, по порядку. Оба сравниваемых экземпляра должны быть одинакового типа. Если order истинно, а eq ложно, возникает ошибка ValueError.

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

  • unsafe_hash: Если False (по умолчанию), генерируется метод __hash__() в соответствии с тем, как заданы eq и frozen.

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

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

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

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

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

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

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

Added in version 3.10.

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

Added in version 3.10.

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

Added in version 3.10.

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

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

Added in version 3.11.

fields может опционально указать значение по умолчанию, используя обычный синтаксис 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 (по умолчанию), это поле будет включено в строку, возвращаемую методом generated __repr__().

  • hash: Это может быть bool или None. Если true, то это поле включается в генерируемый метод __hash__(). Если None (по умолчанию), то используется значение compare: обычно это ожидаемое поведение. Поле должно учитываться в хэше, если оно используется для сравнения. Не рекомендуется устанавливать это значение в любое другое значение, кроме None.

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

  • compare: Если значение равно true (по умолчанию), то это поле включается в генерируемые методы равенства и сравнения (__eq__(), __gt__() и т. д.).

  • метаданные: Это может быть отображение или None. None рассматривается как пустой dict. Это значение оборачивается в MappingProxyType(), чтобы сделать его доступным только для чтения, и отображается на объект Field. В классах данных он не используется вообще, и предоставляется в качестве стороннего механизма расширения. Несколько сторонних объектов могут иметь свой собственный ключ, чтобы использовать его в качестве пространства имен в метаданных.

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

Added in version 3.10.

Если значение поля по умолчанию указано вызовом field(), то атрибут class для этого поля будет заменен указанным значением default. Если значение default не указано, то атрибут class будет удален. Смысл в том, что после выполнения декоратора @dataclass все атрибуты класса будут содержать значения по умолчанию для полей, как если бы было указано само значение по умолчанию. Например, после:

@dataclass
class C:
    x: int
    y: int = field(repr=False)
    z: int = field(repr=False, default=10)
    t: int = 20

Атрибут класса C.z будет равен 10, атрибут класса C.t будет равен 20, а атрибуты классов C.x и C.y не будут установлены.

class dataclasses.Field

Объекты Field описывают каждое определенное поле. Эти объекты создаются внутри модуля и возвращаются методом fields() на уровне модуля (см. ниже). Пользователи никогда не должны инстанцировать объект Field напрямую. Его документированными атрибутами являются:

  • name: Имя поля.

  • type: Тип поля.

  • default, default_factory, init, repr, hash, compare, metadata и kw_only имеют тот же смысл и те же значения, что и в функции field().

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

dataclasses.fields(class_or_instance)

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

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

Преобразует класс данных obj в dict (с помощью фабричной функции dict_factory). Каждый класс данных преобразуется в dict своих полей в виде пар name: value. Классы данных, dicts, списки и кортежи рекурсируются. Другие объекты копируются с помощью 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, то typing.Any используется для type. Значения init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots и weakref_slot имеют тот же смысл, что и в @dataclass.

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

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

Added in version 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__() см. в разделе ниже, посвященном переменным init-only. Также смотрите предупреждение о том, как replace() обрабатывает поля init=False.

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

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

Переменные только для инициализации

Еще одно место, где @dataclass проверяет аннотацию типа, - это определение того, является ли поле переменной init-only. Для этого нужно посмотреть, имеет ли тип поля тип dataclasses.InitVar. Если поле имеет тип InitVar, оно считается псевдополем, называемым init-only field. Поскольку это не настоящее поле, оно не возвращается функцией fields() на уровне модуля. Поля типа init-only добавляются в качестве параметров в генерируемый метод __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, вы можете эмулировать неизменяемость. В этом случае классы dataclasses добавят в класс методы __setattr__() и __delattr__(). При вызове этих методов будет возникать ошибка FrozenInstanceError.

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

Наследство

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

@dataclass
class Base:
    x: Any = 15.0
    y: int = 0

@dataclass
class C(Base):
    z: int = 10
    x: int = 15

Конечный список полей выглядит следующим образом: x, y, z. Конечным типом x является int, как указано в классе C.

Созданный метод __init__() для C будет выглядеть так:

def __init__(self, x: int = 15, y: int = 0, z: int = 10):

Переупорядочивание параметров только для ключевых слов в __init__()

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

В этом примере Base.y, Base.w и D.t - поля, содержащие только ключевые слова, а Base.x и D.z - обычные поля:

@dataclass
class Base:
    x: Any = 15.0
    _: KW_ONLY
    y: int = 0
    w: int = 1

@dataclass
class D(Base):
    z: int = 10
    t: int = field(kw_only=True, default=0)

Созданный метод __init__() для D будет выглядеть так:

def __init__(self, x: Any = 15.0, z: int = 10, *, y: int = 0, w: int = 1, t: int = 0):

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

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

Заводские функции по умолчанию

Если field() задает default_factory, он вызывается с нулевыми аргументами, когда требуется значение по умолчанию для поля. Например, чтобы создать новый экземпляр списка, используйте:

mylist: list = field(default_factory=list)

Если поле исключено из __init__() (с помощью init=False) и в поле также указано default_factory, то из сгенерированной функции __init__() всегда будет вызываться функция фабрики по умолчанию. Это происходит потому, что нет другого способа придать полю начальное значение.

Изменяемые значения по умолчанию

Python хранит значения переменных-членов по умолчанию в атрибутах класса. Рассмотрим этот пример, не использующий классы данных:

class C:
    x = []
    def add(self, element):
        self.x.append(element)

o1 = C()
o2 = C()
o1.add(1)
o2.add(2)
assert o1.x == [1, 2]
assert o1.x is o2.x

Обратите внимание, что два экземпляра класса C имеют одну и ту же переменную класса x, как и ожидалось.

Используя классы данных, если этот код был действителен:

@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__() дескриптора, а не перезаписывает объект дескриптора.

  • Аналогично, при получении или установке поля вызывается метод __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

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