Spec-Zone.ru › Python 3.9

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)

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

Декоратор dataclass() исследует класс, чтобы найти field. field определяется как переменная класса, имеющая аннотацию типа. За исключением двух случаев, описанных ниже, ничего в dataclass() не исследует тип, указанный в аннотации переменной.

Порядок полей во всех сгенерированных методах соответствует порядку их появления в определении класса.

Декоратор dataclass() добавит различные методы «дундер» в класс, как описано ниже. Если какой-либо из добавленных методов уже существует в классе, поведение зависит от параметра, как описано ниже. Декоратор возвращает тот же самый класс, к которому он применяется; новый класс не создается.

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

@dataclass
class C:
    ...

@dataclass()
class C:
    ...

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=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 истинно, а eq ложно, генерируется исключение 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__() в вашем dataclass, так и установить unsafe_hash=True; это приведёт к исключению TypeError.

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

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

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, repr=True, hash=None, init=True, compare=True, metadata=None)

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

@dataclass
class C:
    mylist: list[int] = field(default_factory=list)

c = C()
c.mylist += [1, 2, 3]

Как показано выше, значение MISSING — это объект-маяк, используемый для определения, указаны ли параметры default и default_factory. Этот маяк используется, потому что None является допустимым значением для default. Никакой код не должен напрямую использовать значение MISSING.

Параметры функции field():

  • default: Если указано, это будет значение по умолчанию для данного поля. Это необходимо, потому что сам вызов field() заменяет обычное положение значения по умолчанию.
  • default_factory: Если указано, это должна быть функция без аргументов, которая будет вызываться при необходимости значения по умолчанию для этого поля. Среди прочего, это можно использовать для указания полей с изменяемыми значениями по умолчанию, как обсуждалось ниже. Недопустимо указывать одновременно default и default_factory.
  • init: Если значение True (по умолчанию), это поле включается в качестве параметра для сгенерированного метода __init__().
  • repr: Если значение True (по умолчанию), это поле включается в строку, возвращаемую сгенерированным методом __repr__().
  • compare: Если значение True (по умолчанию), это поле включается в сгенерированные методы равенства и сравнения (__eq__(), __gt__() и т.д.).
  • hash: Может быть bool или None. Если значение True, это поле включается в сгенерированный метод __hash__(). Если None (значение по умолчанию), используется значение compare: это обычно ожидаемое поведение. Поле должно учитываться в хэше, если оно используется для сравнений. Не рекомендуется устанавливать это значение на что-либо отличное от None.

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

  • metadata: Может быть словарем или None. None обрабатывается как пустой словарь. Это значение обернуто в MappingProxyType() для обеспечения его неизменяемости и показано в объекте Field. Оно вообще не используется Data Classes и предоставляется как механизм расширения для сторонних библиотек. Различные сторонние библиотеки могут иметь свои собственные ключи для использования в качестве имени пространства имён в метаданных.

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

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

dataclasses.fields(class_or_instance)

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

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

Преобразует dataclass obj в словарь (с использованием фабричной функции dict_factory). Каждая dataclass преобразуется в словарь её полей, как пары name: value. dataclass, словари, списки и кортежи рекурсивно преобразуются. Другие объекты копируются с помощью copy.deepcopy().

Пример использования asdict() с вложенными dataclass:

@dataclass
class Point:
     x: int
     y: int

@dataclass
class C:
     mylist: list[Point]

p = Point(10, 20)
assert asdict(p) == {'x': 10, 'y': 20}

c = C([Point(0, 0), Point(10, 4)])
assert asdict(c) == {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}

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

dict((field.name, getattr(obj, field.name)) for field in fields(obj))

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

dataclasses.astuple(obj, *, tuple_factory=tuple)

Преобразует dataclass obj в кортеж (с использованием фабричной функции tuple_factory). Каждая dataclass преобразуется в кортеж значений её полей. dataclass, словари, списки и кортежи рекурсивно преобразуются. Другие объекты копируются с помощью 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 не является экземпляром dataclass.

dataclasses.make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)

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

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

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

Сгенерированный код __init__() вызовет метод с именем __post_init__(), если __post_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__():

@dataclass
class Rectangle:
    height: float
    width: float

@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
    database: InitVar[DatabaseType] = 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):

Функции по умолчанию

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

mylist: list = field(default_factory=list)

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

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

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

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, как ожидалось.

Используя dataclasses, если этот код был допустим:

@dataclass
class D:
    x: List = []
    def add(self, element):
        self.x += element

он сгенерировал бы код, аналогичный этому:

class D:
    x = []
    def __init__(self, x=x):
        self.x = x
    def add(self, element):
        self.x += element

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

Это та же проблема, что и в исходном примере, использующем класс C. То есть, два экземпляра класса D, которые не задают значение для x при создании экземпляра класса, будут использовать одну и ту же копию x. Поскольку dataclasses используют обычное создание класса Python, они также демонстрируют такое поведение. Нет общего способа для Data Classes обнаружить это условие. Вместо этого dataclasses будут поднимать TypeError, если обнаружит параметр по умолчанию типа list, dict или set. Это частичное решение, но оно защищает от многих распространённых ошибок.

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

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

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

Исключения

exception dataclasses.FrozenInstanceError

Вызывается, когда неявно определённый __setattr__() или __delattr__() вызывается для dataclass, который был определён с frozen=True. Это подкласс AttributeError.

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

Spec-Zone.ru

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