Сообщить об ошибке.

Декоратор dataclass() модуля dataclasses в Python

Создание класса для хранения пользовательских данных

Синтаксис:

from dataclasses import dataclass

@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 MyType:
...

Параметры:

  • init=True - флаг для создания метода __init__(),
  • repr=True - флаг для создания метода __repr__(),
  • eq=True - флаг для создания метода __eq__(),
  • order=False - флаг для создания методов сравнения класса,
  • unsafe_hash=False - флаг для создания метода __hash__(),
  • frozen=False - флаг для создания неизменяемого типа,
  • match_args=True - создает кортеж __match_args__ из списка аргументов (добавлено в Python 3.10),
  • kw_only=False - помечает все поля класса как содержащие только ключевые слова (добавлено в Python 3.10),
  • slots=False - если True, то генерирует атрибут __slots__ (добавлено в Python 3.10),
  • weakref_slot=False - добавляет слот с именем __weakref__ (добавлено в Python 3.11).

Возвращаемое значение:

  • класс, на основе которого создается тип данных, новый класс не создается.

Описание:

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

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

Декоратор @dataclass() не проверяет, указанный в аннотации, тип переменной!

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

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

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

from dataclasses import 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: если True (по умолчанию), то будет создан метод __init__().

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

  • repr: если True (по умолчанию), будет сгенерирован метод __repr__(). Сгенерированная строка будет иметь имя класса, а также имя и repr каждого поля в том порядке, в котором они определены в классе. Поля, отмеченные как исключенные, не включаются.

    Например: InventoryItem(name = 'widget', unit_price = 3.0, quantity_on_hand = 10).

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

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

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

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

    Если order равен True, а eq - false, возникает ошибка ValueError.

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

  • unsafe_hash: если 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__(). То есть, если суперкласс является объектом, то он вернется к хешированию на основе идентификатора.
  • frozen: если равен True (по умолчанию False), то при попытке изменить значения поля будет генерироваться исключение. Это имитирует frozen экземпляры, доступные только для чтения. Если в классе определены методы __setattr__() или __delattr__(), то возникает ошибка TypeError.

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

    Новое в Python 3.10.

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

    Дополнительно смотрите описание значения dataclasses.KW_ONLY

    Новое в Python 3.10.

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

    Новое в Python 3.10.

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

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

    Новое в Python 3.11.

Поля создаваемого типа могут дополнительно указывать значение по умолчанию, используя обычный синтаксис Python:

@dataclass
class C:
    a: int       # 'a' не имеет значения по умолчанию
    b: int = 0   # 'b' имеет значения по умолчанию 0

В этом примере и a и b будут включены в добавленный метод __init__(), который будет определен как:

def __init__(self, a: int, b: int = 0):

Если поле без значения по умолчанию следует за полем со значением по умолчанию, то будет вызвано исключение TypeError. Это верно либо когда это происходит в отдельном классе, либо в результате наследования классов.

Примеры создания типов для хранения пользовательских данных.

Простое использование декоратора @dataclass.

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int

>>> p = Person('John Doe', 34)
>>> p
# Person(name='John Doe', age=34)

Использование значений по умолчанию.

from dataclasses import dataclass

@dataclass
class Person:
    name: str = 'unknown'
    age: int = 0

>>> p = Person()
>>> p
# Person(name='unknown', age=0)
>>> p1 = Person('John Doe', 34)
>>> p1
# Person(name='John Doe', age=34)

Использование аргумента repr.

При указании аргумента repr=False, создаваемый тип не будет красиво выводится в терминале.

from dataclasses import dataclass 

@dataclass(repr=False) 
class Article(): 
    title: str
    language: str
  
>>> article = Article('DataClasses', 'Python3') 
>>> article
# <__main__.Article object at 0x7f94b4605d60>

Использование аргумента eq.

При указании аргумента eq=False, два объекта сравниваются с использованием их хэша на основе их местоположения в памяти, как два обычных объекта. Поскольку два объекта имеют разное хеш-представление, их равенство возвращает False.

@dataclass(eq=False) 
class Article(): 
    title: str
    language: str
  
# Создадим 2 одинаковых типа данных
>>> article1 = Article('DataClasses', 'Python3')   
>>> article2 = Article('DataClasses', 'Python3')   
# сравним эти типы
>>> article1 == article2 
# False

Использование аргумента order.

Сравнение двух классов данных не только ограничивается равенством, но также поддерживает операторы, >, >=, < и <=, когда в параметре аргумента задано значение order=True.

Сравнение между объектами основано на сравнении соответствующих им атрибутов, которое выполняется по очереди, начиная с первого.

from dataclasses import dataclass  
    
@dataclass(order=True) 
class A(): 
    var1: int
    var2: float
  
>>> obj1 = A(1, 7.0) 
>>> obj2 = A(2, 7.0) 
>>> obj3 = A(1, 7.0) 
>>> obj4 = A(1, 8.0) 
>>> obj1 >  obj2
# False
>>> obj1 ==  obj3 
# True
>>> obj1 >= obj4
# False

Использование аргумента frozen.

Аргумент frozen устанавливает все переменные в классе данных как неизменяемые, которые после инициализации не могут быть переназначены на новое значение. Это похоже на поведение переменной Java, определенной при помощи оператора final или оператора const языка C.

from dataclasses import dataclass  
    
@dataclass(frozen=True) 
class A(): 
    var1: int
    var2: float
  
>>> obj1 = A(1, 7.0)
>>> obj1.var1 = 10
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<string>", line 4, in __setattr__
# dataclasses.FrozenInstanceError: cannot assign to field 'var1'