Как узнать число полей у dataclass класса
Перейти к содержимому

Как узнать число полей у dataclass класса

  • автор:

Декоратор 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__() . То есть, если суперкласс является объектом, то он вернется к хешированию на основе идентификатора.

    Поля создаваемого типа могут дополнительно указывать значение по умолчанию, используя обычный синтаксис 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 #

    Использование аргумента 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 .

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

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

    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 "", line 1, in # File "", line 4, in __setattr__ # dataclasses.FrozenInstanceError: cannot assign to field 'var1' 
    • КРАТКИЙ ОБЗОР МАТЕРИАЛА.
    • Декоратор dataclass() модуля dataclasses
    • Функция field() модуля dataclasses
    • Функция asdict() модуля dataclasses
    • Функция astuple() модуля dataclasses
    • Функция make_dataclass() модуля dataclasses
    • Функция replace() модуля dataclasses
    • Функция is_dataclass() модуля dataclasses
    • Функция fields() модуля dataclasses
    • Значение KW_ONLY модуля dataclasses

    Классы данных в Python и их ключевые особенности

    Значимым компонентом любого проекта в программировании являются данные, с которыми неизбежно взаимодействуют все программы. Например, при разработке веб-сайта вы должны представить тексты и изображения в удобном для пользователя виде. Если же в вашем проекте для прогноза тенденций в области финансов используется МО, то не обойтись без подготовки данных для обучения ваших моделей.

    Будучи многофункциональным языком программирования, Python располагает универсальным набором инструментов, поддерживающих управление данными в проектах. В их число входит такой полезный и простой в освоении метод, как классы данных. В этой статье вы узнаете об основных особенностях этих классов и научитесь эффективно задействовать их в своей работе.

    1. Определение класса данных

    Функциональность класса данных определяется в модуле dataclasses , который входит в состав стандартной библиотеки Python. Его важнейшей частью является декоратор dataclass . Следующий код иллюстрирует применение этого декоратора с пользовательским классом, который мы определили для управления счетами в ресторане.

    from dataclasses import dataclass


    @dataclass
    class Bill:
    table_number: int
    meal_amount: float
    served_by: str
    tip_amount: float = 0.0

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

    У вас может возникнуть вопрос о целесообразности применения dataclass . Аргументом в его пользу послужит тот факт, что он помогает избавиться от таких стандартных методов, как __init__ и __repr__ . Другими словами, благодаря декоратору нам не приходится реализовывать эти методы, поскольку он это делает за нас, как показано ниже в примере инициализации и представления:

    >>> bill0 = Bill(table_number=5, meal_amount=50.5, served_by="John", tip_amount=7.5)
    >>> print(f"Today's first bill: ")
    Today's first bill: Bill(table_number=5, meal_amount=50.5, served_by='John', tip_amount=7.5)

    Как видим, несмотря на отсутствие явного определения, методы инициализации и представления реализуются идиоматически.

    2. Аннотации типов и значения полей по умолчанию

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

    Итак, поля класса данных должны содержать аннотации типов. В противном случае ваш код не будет выполняться. Внутренне декоратор dataclass генерирует поля на основе аннотаций, которые можно получить с помощью специального метода __annotations__ .

    >>> Bill.__annotations__
    , 'meal_amount': , 'served_by': , 'tip_amount': >

    Также следует обратить внимание на то, что определяя поля для класса данных, вы можете установить для них значения по умолчанию. Как вы могли заметить, поле tip_amount содержит подобное значение — 0.0. При этом важно помнить, что если значения по умолчанию определены не для всех полей, то поля, имеющие эти значения, должны следовать за полями без таковых. Иначе вы можете столкнуться со следующей ошибкой:

    @dataclass
    . class SpecialBill:
    . table_number: int = 888
    . meal_amount: float
    . served_by: str
    . tip_amount: float = 0.0
    .
    Traceback (most recent call last):
    . some traceback info omitted
    TypeError: non-default argument 'meal_amount' follows default argument

    3. Проверка равенства/неравенства

    Помимо методов инициализации и представления декоратор dataclass также обеспечивает выполнение операций сравнения. Как известно, в случае с обычным пользовательским классом мы не можем полноценно сравнивать экземпляры без определения соответствующего поведения. Рассмотрим следующий пользовательский класс без dataclass :

    >>> old_bill0 = OldBill(table_number=3, meal_amount=20.5, served_by="Dan", tip_amount=5)
    . old_bill1 = OldBill(table_number=3, meal_amount=20.5, served_by="Dan", tip_amount=5)
    . print("Comparison Between Regular Instances:", old_bill0 == old_bill1)
    .
    Comparison Between Regular Instances: False
    >>> bill0 = Bill(table_number=5, meal_amount=50.5, served_by="John", tip_amount=7.5)
    . bill1 = Bill(table_number=5, meal_amount=50.5, served_by="John", tip_amount=7.5)
    . print("Comparison Between Data Class Instances:", bill0 == bill1)
    .
    Comparison Between Data Class Instances: True

    Как показано выше, в обычном классе два экземпляра с одинаковыми значениями всех атрибутов рассматриваются как неравные, поскольку по умолчанию сравниваются их идентичности. В данном случае эти два экземпляра являются разными объектами, в связи с чем и считаются неравными.

    Однако в классе данных при таком же способе сравнения равенство подтверждается True . Объясняется это тем, что декоратор dataclass также автоматически сгенерирует специальный метод __eq__ . Дело в том, что при выполнении сравнения для выявления равенства каждый из этих экземпляров рассматривается как кортеж, содержащий поля в установленном порядке. И поскольку два экземпляра класса данных имеют поля с одинаковыми значениями, то они считаются равными.

    >>> @dataclass(order=True)
    . class ComparableBill:
    . meal_amount: float
    . tip_amount: float
    .
    .
    . bill1 = ComparableBill(meal_amount=50.5, tip_amount=7.5)
    . bill2 = ComparableBill(meal_amount=50.5, tip_amount=8.0)
    . bill3 = ComparableBill(meal_amount=60, tip_amount=10)
    . print("Is bill1 less than bill2?", bill1 < bill2)
    . print("Is bill2 less than bill3?", bill2 < bill3)
    .
    Is bill1 less than bill2? True
    Is bill2 less than bill3? True

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

    4. Проблема изменяемости

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

    >>> from collections import namedtuple
    >>> NamedTupleBill = namedtuple("NamedBill", "meal_amount tip_amount")
    >>>
    >>> @dataclass
    . class DataClassBill:
    . meal_amount: float
    . tip_amount: float
    .
    >>> namedtuple_bill = NamedTupleBill(20, 5)
    >>> dataclass_bill = DataClassBill(20, 5)
    >>> namedtuple_bill.tip_amount = 6
    Traceback (most recent call last):
    File "", line 1, in
    AttributeError: can't set attribute
    >>> dataclass_bill.tip_amount = 6

    Как следует из примера выше, мы не можем установить атрибут именованного кортежа. Зато это можно сделать применительно к экземпляру класса данных, что подчеркивает преимущества поддержки изменяемости таких классов.

    Однако изменяемость данных не всегда кстати, и в ряде случаев хотелось бы обойтись без нее. Для этого, применяя декоратор dataclass , установите для параметра frozen значение True . Ниже рассмотрим простой пример “замороженного” экземпляра класса данных:

    >>> @dataclass(frozen=True)
    . class ImmutableBill:
    . meal_amount: float
    . tip_amount: float
    .
    >>> immutable_bill = ImmutableBill(20, 5)
    >>> immutable_bill.tip_amount = 7
    Traceback (most recent call last):
    File "", line 1, in
    File "", line 4, in __setattr__
    dataclasses.FrozenInstanceError: cannot assign to field 'tip_amount'

    5. Проблемы наследования

    По своей сути, класс данных обладает такой же расширяемостью, что и обычный пользовательский класс. Следовательно, при необходимости на его основе мы можем создать его подкласс.

    >>> @dataclass
    . class BaseBill:
    . meal_amount: float
    .
    >>>
    >>> @dataclass
    . class TippedBill(BaseBill):
    . tip_amount: float
    .
    >>> tipped_bill = TippedBill(meal_amount=20, tip_amount=5)

    Как следует из данного фрагмента кода, декоратор dataclass задействует поля базового класса для создания подкласса. Другими словами, инициализация подкласса свидетельствует о том, что он автоматически получает сигнатуру полей базового класса и самого подкласса. Обратите внимание, что при наличии нескольких уровней потомков очередность основана на порядке определения — сначала идут поля базового класса, за которыми следуют поля подкласса и так далее.

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

    @dataclass
    . class BaseBill:
    . meal_amount: float = 20
    .
    @dataclass
    . class TippedBill(BaseBill):
    . tip_amount: float
    .
    Traceback (most recent call last):
    . some traceback info omitted
    TypeError: non-default argument 'tip_amount' follows default argument

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

    В предшествующих примерах все поля являются изменяемыми типами данных, например float (число с плавающей точкой) и string (строка). Что же стоит предпринять при необходимости использовать изменяемые данные в качестве полей класса данных? Рассмотрим следующий фрагмент кода, содержащий ряд возможных ошибок:

    >>> class IncorrectBill:
    . def __init__(self, costs_by_dish=[]):
    . self.costs_by_dish = costs_by_dish
    .
    >>> bill0 = IncorrectBill()
    >>> bill1 = IncorrectBill()
    >>> bill0.costs_by_dish.append(5)
    >>> bill1.costs_by_dish.append(7)
    >>> print("Bill 0 costs:", bill0.costs_by_dish)
    Bill 0 costs: [5, 7]
    >>> print("Bill 1 costs:", bill1.costs_by_dish)
    Bill 1 costs: [5, 7]
    >>> bill0.costs_by_dish is bill1.costs_by_dish
    True

    Данный код показывает, что в случае применения в функции предустановленного изменяемого значения, фактически вы не можете установить такое значение с помощью изменяемого экземпляра, поскольку этот объект будет совместно использоваться всеми вызывающими компонентами, в которых данный параметр не определяется. В вышеуказанном примере оба экземпляра задействуют одинаковый объект списка, что приводит к нежелательным побочным эффектам.

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

    @dataclass
    class IncorrectBill:
    costs_by_dish: list = []ValueError: mutable default for field costs_by_dish is not allowed: use default_factory

    Сообщение об ошибке подсказывает решение, подразумевающее применение default_factory при определении поля. Вот пример:

    >>> from dataclasses import field
    . @dataclass
    . class CorrectBill:
    . costs_by_dish: list = field(default_factory=list)
    .
    >>> bill0 = CorrectBill()
    >>> bill0.costs_by_dish.append(5)
    >>> bill1 = CorrectBill()
    >>> bill1.costs_by_dish.append(7)
    >>> print("Bill 0 costs:", bill0.costs_by_dish)
    Bill 0 costs: [5]
    >>> print("Bill 1 costs:", bill1.costs_by_dish)
    Bill 1 costs: [7]
    >>> bill0.costs_by_dish is bill1.costs_by_dish
    False
    • Из модуля dataclasses импортируем функцию field .
    • В функции field определяем list в качестве параметра default_factory . По сути, этот параметр устанавливает функцию default_factory , т. е. функцию с нулевым параметром, использующуюся при создании экземпляра. В данном случае функция list является методом конструктора для объекта списка, создающим этот список при вызове list() .
    • Как видим, оба экземпляра располагают разными объектами списков для атрибута costs_by_dish , на что мы и рассчитывали.

    Заключение

    В данной статье был проведен обзор 6 самых основных особенностей классов данных в Python. В сущности, они весьма эффективны для хранения данных, а декоратор dataclass выполняет за нас все рутинные процедуры. По сравнению с именованными кортежами классы данных отличаются большей гибкостью благодаря изменяемости.

    • Создание собственной симуляции активной материи на Python
    • Создание простой нейронной сети на Python
    • Лучший алгоритм решения задач по программированию на Python

    DataClass в Python

    Конструкции dataclass доступны в Python 3.7 и старше. Их можно использовать как контейнеры данных и не только. Конструкции dataclass позволяют писать шаблонный код и упрощают процесс создания классов, ведь в них для этого есть специальные методы.

    Первый Dataclass

    Создадим DataClass, представляющий собой точку в трехмерной системе координат.

    Декоратор @dataclass используется для создания Data Class. x , y и z являются его полями. Стоит отметить, что с ними необходимо использовать определения типов данных. При этом они не являются объявлениями статического типа, и кто угодно может передать данные любого типа кроме int полям x , y или z .

    from dataclasses import dataclass @dataclass class Coordinate: x: int y: int z: int 

    По умолчанию у Data Class есть методы __init__ , __repr__ и __eq__ , поэтому их не нужно реализовывать самостоятельно.

    И пусть __init__ , __repr__ и __eq__ не реализованы в классе Coordinate их по-прежнему можно использовать благодаря dataclass . Это здорово экономит время.

    from dataclasses import dataclass @dataclass class Coordinate: x: int y: int z: int a = Coordinate(4, 5, 3) print(a) # результат: Coordinate(x=4, y=5, z=3) 

    Значения по умолчанию для полей

    Полям можно присваивать значения по умолчанию. Используем пример. Как видно по полю pi , можно запросто присвоить значение по умолчанию для полей в DataClass.

    from dataclasses import dataclass @dataclass class CircleArea: r: int pi: float = 3.14 @property def area(self): return self.pi * (self.r ** 2) a = CircleArea(2) print(repr(a)) # вернется: CircleArea(r=2, pi=3.14) print(a.area) # вернется: 12.56 

    Изменение полей и DataClass

    Изменять поля и DataClass можно за счет параметров декоратора или функции поля. Все случаи будут рассмотрены в примерах.

    Data Class: изменяемые или нет?

    По умолчанию dataclass изменяемые, что значит, что полям можно присваивать значения. Но их же можно сделать и неизменяемыми, задав значение True для параметра frozen .

    Изменяемый пример

    from dataclasses import dataclass @dataclass class CircleArea: r: int pi: float = 3.14 @property def area(self): return self.pi * (self.r ** 2) a = CircleArea(2) a.r = 5 print(repr(a)) # вернется: CircleArea(r=5, pi=3.14) print(a.area) # вернется: 78.5 

    Неизменяемый пример

    Когда значение frozen равно True , присваивать значения полям больше нельзя. Дальше показано соответствующее исключение.

    from dataclasses import dataclass @dataclass(frozen=True) class CircleArea: r: int pi: float = 3.14 @property def area(self): return self.pi * (self.r ** 2) a = CircleArea(2) a.r = 5 # Вернется исключение: dataclasses.FrozenInstanceError: # cannot assign to field 'r' 

    Сравнение Data Classes

    Представьте, что вы хотите создать DataClass, представляющий Vector и сравнить их. Как это сделать? Для сравнения нужны методы __lt__ или __gt__ .

    По умолчанию значение параметра order в Data Class равняется False . Если поменять его на True , то методы __lt__ , __le__ , __gt__ и __ge__ для Data Class будут сгенерированы автоматически. Таким образом можно сравнить объекты так, будто бы они являются кортежами полей.

    Разберем на примере. Задав order значение True , можно сравнить v2 и v1 . Но есть проблема логики сравнения. Если сравнить, например, v2 > v1 , то векторы будут сравниваться вот так: (8, 15) > (7, 20) . Таким образом значением операции будет True .

    Стоит напомнить, что сравнение кортежей происходит по порядку. Сначала сравниваются 8 и 7, и если результат равен True , то результатом всего сравнения будет True . Если бы они были равны, то произошло бы сравнение 15 > 20 и результат стал бы False .

    from dataclasses import dataclass, field @dataclass(order=True) class Vector: x: int y: int v1 = Vector(8, 15) v2 = Vector(7, 20) print(v2 > v1) 

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

    В этом случае можно воспользоваться функцией field и методом __post_init__ . Функция field позволит настроить поле magnitude , а __post_init__ — определить величину вектора после инициализации.

    Поле magnitude было изменено с помощью функции field из Data Class. Задавая значение False для init , мы утверждаем, что не хотим иметь параметр magnitude в методе __init__ . Потому что его значение можно задать с помощью метода __post_init__ после инициализации.

    Конвертация в словарь или кортеж

    Можно получить атрибуты Data Class в кортеже или словаре. Для этого нужно лишь импортировать функции asdict и astuple из Data Class.

    from dataclasses import dataclass, asdict, astuple @dataclass class Vector: x: int y: int z: int v = Vector(4, 5, 7) print(asdict(v)) # Вернется : print(astuple(v)) # Вернется : (4, 5, 7) 

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

    Можно создавать подклассы для Data Class как для обычных классов в Python.

    from dataclasses import dataclass @dataclass class Employee: name: str lang: str @dataclass class Developer(Employee): salary: int Alex = Developer('Alex', 'Python', 5000) print(Alex) # Вернется: Developer(name='Alex', lang='Python', salary=5000) 

    Но есть распространенная ошибка при использовании наследования. Когда значением поля lang по умолчанию является Python, нужно задавать значения по умолчанию для полей следом за lang .

    from dataclasses import dataclass @dataclass class Employee: name: str lang: str = 'Python' @dataclass class Developer(Employee): salary: int Alex = Developer('Alex', 'Python', 5000) # Вернется: TypeError: non-default argument 'salary' follows default argument 

    Чтобы понять, зачем это нужно, рассмотрим, как выглядит метод __init__ . Вспомним, что параметры со значением по молчанию всегда идут после параметров без них.

    def __init__(name: str, lang: str = 'Python', salary: int): ... 

    Исправим, задав значение по умолчанию для поля salary .

    from dataclasses import dataclass @dataclass class Employee: name: str lang: str = 'Python' @dataclass class Developer(Employee): salary: int = 0 Alex = Developer('Alex', 'Python', 5000) print(Alex) # Вернется: Developer(name='Alex', lang='Python', salary=5000) 

    Преимущества слотов

    По умолчанию атрибуты хранятся в словаре. Можно получить преимущество от слотов для более быстрого доступа к атрибутам и использовать меньше памяти. Однако в подробностях эта тема здесь рассматриваться не будет.

    from dataclasses import dataclass @dataclass class Employee: name: str lang: str Alex = Employee('Alex', 'Python') print(Alex.__dict__) # 

    Слоты нужны для использования меньшего количества памяти и более быстрого доступа к атрибутам.

    from dataclasses import dataclass @dataclass class Employee: __slots__ = ('name', 'lang') name: str lang: str Alex = Employee('Alex', 'Python') 

    Параметры Dataclass

    Некоторые параметры в декораторе Data Class были изменены для настройки Data Class. Вот они:

    • init : метод __init__ будет сгенерирован в Data Class при значении True . ( True – значение по умолчанию)
    • repr : метод __repr__ будет сгенерирован в Data Class при значении True . ( True – значение по умолчанию)
    • eq : метод __eq__ будет сгенерирован в Data Class при значении True . ( True – значение по умолчанию)
    • order : методы __lt__ , __le__ , __gt__ и __ge__ будут сгенерированы в Data Class при значении True . ( False – значение по умолчанию)
    • unsafe_hash : метод __hash__ будет сгенерирован в Data Class при значении True . ( False – значение по умолчанию)
    • frozen : если True , то значения полям присваивать нельзя ( False – значение по умолчанию).

    Примечание: eq должно быть равно True , если значение order равняется True , иначе будет исключение ValueError .

    Параметры полей

    • init : это поле включено в сгенерированный метод __init__ при значении True ( True – значение по умолчанию)
    • repr : это поле включено в сгенерированный метод __init__ при значении True ( True – значение по умолчанию)
    • compare : это поле включено в методы общего сравнения и равенства при значении True ( True – значение по умолчанию)
    • hash : это поле включено в сгенерированный метод __hash__ при значении True ( None – значение по умолчанию)
    • default : это будет значение по умолчанию для поля (если указано)
    • default_factory : если указано, должно быть вызываемым объектом без аргументов, который вызывается, когда для поля требуется значение по умолчанию
    • metadata : это может быть мапинг или None – пустой словарь.

    Dataclass’ы в Python

    В Python существует множество структур данных, каждая из которых обычно заточена и максимально эффективна лишь в конкретных, присущих только ей, ситуациях. Некоторые структуры, такие как списки, словари и кортежи могут быть вызваны прямо из интерпретатора, а некоторые, наподобие именованных кортежей и attrs проектов, нуждаются в установке дополнительных пакетов Python. Но, независимо от многообразия, имеющихся в Python структур данных, одной из наиболее универсальных, многофункциональных и, в тоже время потенциально невостребованных структур, являются все-таки классы данных. О нюансах применения и основных потенциальных возможностях, как раз и пойдет речь ниже в этой статье.

    Классы данных являются объектами, схожими на обычные классы в Python, но специализирующимися, главным образом, на создании и хранении данных. Возможность использования данных классов появилась лишь в Python 3.7, благодаря использованию декоратора @dataclass из модуля dataclasses:

    from dataclasses import dataclass @dataclass class Currency: country: str USD: float 

    Этот декоратор фактически добавляет к классу данных весь базовый функционал, который отличает его от обычных классов в Python. В частности, данный декоратор автоматически формирует за нас такие «магические» методы, как __repr__ или __eq__:

    pound_sterling = Currency('United Kingdom', 1.22) pound_sterling # Currency(country='United Kingdom', USD=1.22) pound_sterling.USD # 1.22 pound_sterling == Currency('United Kingdom', 1.22) # True 

    Кроме того, при создании класса данных нам также не нужно явно объявлять метод __init__, что существенно сокращает код, позволяя избежать шаблонного кода, который был обязателен при создании обычного класса:

    class Currency: def __init__(self, country, USD): self.country = country self.USD = USD 

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

    pound_sterling = Currency('United Kingdom', 1.22) pound_sterling # pound_sterling == Currency('United Kingdom', 1.22) # False 

    Но, на этом преимущества классов данных не заканчиваются.

    Аннотации типов

    Из предыдущих примеров вы наверняка заметили, что при инициализации свойств в классе данных, после имени соответствующей переменной через двоеточие мы указывали типы данных. Данный синтаксис, доступный начиная с версии Python 3.6 обозначает аннотацию типов, которая достаточно полезна при выявлении соответствующих ошибок. Так, если мы попробуем присвоить значение, которое не соответствует заданному в классе типу, то интерпретатор предупредит нас об этом:

    Yen = Currency('Japan', 'Don`t know') Expected type 'float', got 'str' instead 

    Кроме того, в случае затруднений в определении фактического типа данных при инициализации конкретного экземпляра класса данных, для аннотации типов мы можем воспользоваться типом Any из модуля typing:

    from dataclasses import dataclass from typing import Any, Union @dataclass class Settings: default_user: Union[int, str] # int or str random_seed: Any # Any type 
    Значения по умолчанию

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

    from dataclasses import dataclass @dataclass class AccidentStatus: days_without: int = 0 total_number: int = 0 last: str = 'None' 

    Процесс инициализации свойств классов данных значениями по умолчанию абсолютно идентичен тем же действиям, производящимся в методе __init__ для обычного класса:

    nuclear_plant = AccidentStatus() AccidentStatus(days_without=0, total_number=0, last='None') 
    Методы

    Классы данных поддерживают методы точно так же, как и обычные классы:

    @dataclass class AccidentStatus: days_without: int = 0 total_number: int = 0 last: str = 'None' def new_accident(self, last: str): self.days_without = 0 self.total_number += 1 self.last = last nuclear_plant = AccidentStatus(days_without=21) nuclear_plant.new_accident('Donut without glaze') AccidentStatus(days_without=0, total_number=1, last='Donut without glaze') 

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

    С тем, чтобы на практике рассмотреть основные потенциальные возможности классов данных, давайте попробуем создать класс Bit, где пропишем тип и размер конкретных насадок (битов) для отвертки и, класс BitSet с набором соответствующих насадок в виде списка экземпляров класса Bit. Допустим, что у нас в наборе может быть лишь насадка типа ( + ) с наконечником под крестообразный шлиц и насадка типа ( — ) с наконечником под прямой шлиц. Размер же насадок в наборе может колебаться от 3-х до 8 мм с шагом 1 мм, как для крестообразного, так для прямого шлица.

    from dataclasses import dataclass from typing import List @dataclass class Bit: type: str size: int @dataclass class BitSet: set: List[Bit] 

    Теперь можно легко создать набор насадок для отвертки (бит) на основе выше заданных критериев с помощью простой функции:

    def make_default_set(): return [Bit('-', i) for i in range(3, 8)] + [Bit('+', j) for j in range(3, 8)] 

    Казалось бы, той же функцией можно задать и значение по умолчанию, однако, в этом случае мы столкнемся с ситуацией так называемого Anti-Patternизменяемого аргумента по умолчанию. Суть этой ситуации сводится к тому, что в случае изменений в списке для одного экземпляра нашего класса, те же изменения автоматически появятся и во всех его остальных экземплярах. В обычном классе данная проблема решается путем создания нового списка для каждого из его экземпляров. Но, для этого в данном классе в отношении списка, подверженного изменениям, сначала приходится устанавливать специальное значение по умолчанию, приравниваемое к None, а затем, в методе __init__ еще и обрабатывать это значение с помощью следующей конструкции:

    if arg == None: arg = [. ] 

    В классах же данных эта проблема решается гораздо проще – за счет функции field() в сочетании с ее поименованным аргументом default_factory:

    from dataclasses import dataclass, field from typing import List @dataclass class BitSet: set: List[Bit] = field(default_factory = make_default_set) 

    Теперь, можно убедиться, что проблема решена:

    standard_set = BitSet() my_set = BitSet() my_set.set[0] = Bit('+', 6) # BitSet(set=[Bit(type='+', size=6), . ] 

    Значением поименованного аргумента default_factory может являться любая функция, не принимающая аргументов.

    Кроме default_factory, у функции field() есть еще множество других поименованных аргументов, полный перечень которых приведен ниже:

    • default — Задает значение по умолчанию
    • default_factory — Задает функцию, которая должна возвращать значение по умолчанию
    • init — Указывает, присутствие (видимость) поля при создании экземпляра в __init__(). По умолчанию — True.
    • repr — Указывает, видимость поля при строковом представлении по результатам __repr__(). По умолчанию — True.
    • compare — Указывает, использовать ли поле при сравнениях в __cmp__(). По умолчанию — True.
    • hash — Указывает, учитывать ли поле при вычислении хеша? По умолчанию равно compare.
    • metadata — Добавляет к полю информацию, расширяющую сведения о нем

    На первый взгляд может показаться, что поименованный аргумент default дублирует и без того существующую возможность инициализации значения по умолчанию при помощи обычного оператора присвоения. Вместе с тем, это не совсем так, поскольку применение аргумента default, предоставляет еще и ряд дополнительных бонусов. К примеру, благодаря нему можно ограничить возможность переназначения поля, таким образом, чтобы его невозможно было изменить при инициализации нового экземпляра класса.

    from dataclasses import dataclass, field @dataclass class Order: order_number: int acknowledgement: str = field(default='Не добавлять', init=False) 

    Теперь при создании нового экземпляра класса мы не сможем добавить слова благодарности.

    order = Order(444, acknowledgement='Большое спасибо!') # TypeError: __init__() got an unexpected keyword argument 'acknowledgement' order = Order(444) order.acknowledgement = ' Большое спасибо!' # Order(order_number=444, acknowledgement=' Большое спасибо!') 

    Довольно интересным также является поименованный аргумент metadata, позволяющий прикреплять к полю дополнительную информацию, которую затем можно отображать с помощью функции fields():

    from dataclasses import dataclass, field, fields @dataclass class Warehouse: boxes: int = field(default=0, metadata='size': '30*40*60', 'color': 'white'>) barrels: int = field(default=0, metadata='height': '100', 'color': 'grey'>) fields(Warehouse)[0].metadata['size'] # 30*40*60 

    Репрезентация

    В классах данных предусмотрены такие методы, как obj.__repr__() и obj.__str__(), которые позволяют отображать информацию из этих класса в удобном для разработчиков или пользователей виде. При этом метод obj.__repr__() призван возвращать информацию адаптированную для разработчиков и воссоздающую (по возможности) свой собственный экземпляр, а метод obj.__str__(), по сути дублирует .repr, но предполагает свою реализацию таким образом, чтобы возвращаемая им информация была бы более дружественной для пользователей.

    Давайте на примере, рассмотренного в предыдущем разделе нашей статьи, класса Bit , попытаемся изменить метод .str так, чтобы он был более дружественным для пользователей и возвращал бы сугубо лишь имеющиеся у нас в этом классе данные по типу и размеру насадок (бит) для отверток:

    from dataclasses import dataclass @dataclass class Bit: bit_type: str bit_size: int def __str__(self): return f'self.bit_type>self.bit_size>' 

    Теперь настало время изменить класс BitSet с набором для отверток (списком экземпляров класса Bit ), таким образом, чтобы из него возвращалось бы только его название и список с характеристиками, имеющихся в этом классе (наборе) бит (насадок для отвертки):

    from dataclasses import dataclass, field @dataclass class BitSet: bit_set: List[Bit] = field(default_factory=make_default_set) def __str__(self): set_str = ', '.join(f'b!s>' for b in self.bit_set) return f'self.__class__.__name__>: set_str>' BitSet() # BitSet: -3, -4, -5, -6, -7,-8, +3, +4, +5, +6, +7,+8, 

    Очевидно, что такая запись для пользователей будет гораздо более понятной, чем та которая раннее выводилась базовым методом .repr. Как вы могли видеть, в методе .str при форматировании вывода мы использовали конструкцию . Данная конструкция позволяет выводить характеристики насадок в классе Bit в строковом представлении.

    Сравнение

    Используемый при создании классов данных декоратор @dataclass, который раннее использовался нами без каких-либо параметров, на самом деле может иметь следующий арсенал поименованных аргументов:

    • init — Указывает на необходимость добавления метода .__init__(). (По умолчанию True).
    • repr — Разрешает добавлять метод .__repr__(). (По умолчанию True).
    • eq — Разрешает добавлять метод .__eq__(). (По умолчанию True).
    • order — Для экземпляров класса дает возможность их сортировки и сравнения. (По умолчанию False).
    • unsafe_hash — Добавляет метод .__hash__(). (По умолчанию False).
    • frozen — Обеспечивает неизменяемость класса. Т.е., если данный аргумент установлен в True, то при изменении свойств в экземпляре данного класса интерпретатор выдает ошибку. (По умолчанию False).

    Давайте посмотрим, как инициализация поименованного аргумента order значением True позволит нам сравнивать экземпляры класса Order между собой.

    from dataclasses import dataclass, field @dataclass(order=True) class Order: order_number: int acknowledgement: str = field(default='Not added', init=False) order = Order(444) next_order = Order(445) order  next_order # True 

    В вышеприведенном примере сравнение заказов в экземплярах класса Order происходит лишь по первому его свойству order_number . Вместе с тем существуют ситуации, когда экземпляры классов данных приходится сравнивать и сортировать исходя сразу из нескольких их свойств. С этой целью в классах данных можно применить дополнительное свойство sort_index, инициализацию значения которого нужно задать в специальном методе __post_init__().

    Давайте посмотрим, как это работает на примере уже знакомого нам класса Bit с характеристиками насадок для отверток. Допустим, нам нужно отсортировать насадки так, так, чтобы первыми шли насадки типа ( — ), а вторыми — типа ( + ). Но, загвоздка тут в том, что код ASCII для символа ( — ) составляет 45, а для символа ( + ) — 43. Т.е. автоматическая сортировка по алфавиту, а также сравнение ( — ) < ( + ), в нашем случае будут давать ложный результат. При всей кажущейся сложности, на самом деле выход из вышеописанной ситуации основывается лишь на создании дополнительного списка bits, где требуется просто указать необходимую нам последовательность сортировки наших значений. Затем же, исходя из индексов элементов списка bits в методе __post_init__() мы просто должны организовать расчет значений для инициализации свойства sort_index:

    from dataclasses import dataclass, field bits = ['-', '+'] @dataclass(order=True) class Bit: sort_index: int = field(init=False, repr=False) bit_type: str bit_size: int def __post_init__(self): self.sort_index = (bits.index(self.bit_type) * 100 + self.bit_size) def __repr__(self): return f'self.bit_type>self.bit_size>' Bit('+', 4) > Bit('-', 4) # True sorted([Bit('+', 4), Bit('+', 3), Bit('-', 1)]) # [-1, +3, +4] 

    В нашем примере сразу же после инициализации экземпляра класса Bit, его самое первое свойство sort_index автоматически должно устанавливаться в значение, рассчитываемое в методе __post_init__(). Умножая в этом методе индекс соответствующего элемента на 100, мы тем самым гарантируем, что сортировка, прежде всего, будет учитывать тип насадки, а затем уже корректироваться исходя из ее прибавляемого размера. Таким образом, когда в примере мы сравниваем два экземпляра класса Bit(‘+’, 4) > Bit(‘-‘, 4) , фактически же сравниваются значения этих экземпляров 104 и 4, которые были рассчитаны методом __post_init__() и записаны в свойство sort_index при их инициализации.

    Неизменяемые классы данных

    Достаточно интересная особенность возникает у класса данных при использовании им декоратора @dataclass совместно с его поименованным аргументом frozen. Ведь применение данного аргумента, фактически является альтернативой именованным кортежам в плане возможности создания неизменяемых структур данных (классов):

    from dataclasses import dataclass @dataclass(frozen=True) class FrozenBit: bit_type: str bit_size: int first_bit = FrozenBit('-', 3) first_bit.bit_type = '+' # dataclasses.FrozenInstanceError: cannot assign to field 'bit_type' 

    Благодаря инициализации аргумента frozen значением True, мы уже не сможем изменить в существующем экземпляре first_bit ни одну из характеристик насадки (биты), заданных при инициализации этого экземпляра. Однако, если в создаваемом таким образом классе у вас есть многоуровневые вложенные структуры наподобие словарей, списков или же иных классов данных с изменяемыми элементами, то все эти элементы таковыми и останутся. Поэтому, в случае необходимости распространения неизменяемости на все вложенные элементы класса придется позаботиться об этом отдельно, например, за счет замени списков кортежами.

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

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

    from dataclasses import dataclass, field @dataclass(order=True) class Order: order_number: int acknowledgement: str = field(default='Not added', init=False) @dataclass class DeliveryOrder(Order): address: str 

    При наследовании важно иметь в виду, что если в родительском классе есть свойства со значениями по умолчанию, то все эти же свойства для дочерних класса также должны иметь значения по умолчанию. В противном случае, интерпретатор нам выдаст ошибку non-default argument follows default argument. Это вызвано тем, что с одной стороны в дочернем классе всегда сначала инициализируются родительские методы и свойства, а с другой стороны, не инициализированные параметры (аргументы без значения по умолчанию) в Python всегда также должны обрабатываться первыми. Также важно понимать, что меняя порядок следования свойств и методов в дочернем классе, на самом деле этот порядок остается таким же, каким был зафиксирован в родительском классе.

    Слоты

    Еще одной довольно весомой возможностью классов данных является использование так называемых слотов. Данная возможность за счет задания для вышеназванных классов четко фиксированного числа свойств, может увеличить для этих классов скорость их обработки и уменьшить объем используемой ими памяти.

    Попробуем создать простой класс данных с использованием слотов:

    from dataclasses import dataclass, field @dataclass class SlotsBit: __slots__ = ['bit_type', 'bit_size'] bit_type: str bit_size: int 

    Обратите внимание, что список переменных в __slots__ должен содержать все используемые в классе свойства, которые, при этом, не должны иметь значений по умолчанию. Такие ограничения как раз и позволяют существенно уменьшить потребление памяти и увеличить скорость работы программы. В частности, для сопоставления объема потребляемой памяти для класса со слотами и класса без них можно воспользоваться Python модулем Pympler:

    from pympler import asizeof dc = Bit('+', 4) slots_dc = SlotsBit('+', 4) asizeof.asizeof(dc) # 464 asizeof.asizeof(slots_dc) # 136 

    Используя же Python модуль timeit можно оценить скорость выполнения операций для двух выше обозначенных классов:

    from timeit import timeit timeit('dc.bit_size', setup="dc=Bit('+', 4)", globals=globals()) # 0.025671799999999995 timeit('slots_dc.bit_size', setup="slots_dc=SlotsBit('+', 4)", globals=globals()) # 0.02028190000000002 

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

    Заключение

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

    В этой статье мы рассмотрели вопросы:

    • создания своих классов данных
    • установки в этих классах значений по умолчанию
    • многообразной настройки классы данных
    • реализации наследования в вышеназванных классах

    Для получения дополнительной информации о классах данных, вы можете ознакомиться с PEP 557 или же посмотреть обсуждения по поводу применения этих классов в репозитории, посвященного им проекта на

    Практический Python для начинающих

    Практический Python для начинающих

    Станьте junior Python программистом за 7 месяцев

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *