Изучение преимуществ @dataclass

Почему @dataclass?

Моим первым языком был FORTRAN, который более или менее непригоден для распределенных вычислений из-за глобальных переменных. Тогда я поклялся избегать глобальных переменных, если смогу.

Я выбрал объектно-ориентированный (LOOPS — Lisp Library) из-за его способности инкапсулировать состояния данных. Передача всех инкапсулированных данных была выполнена путем передачи экземпляра объекта (в памяти).

В течение тридцати лет я использовал объекты или классы на каждом языке. Я стесняюсь множественного наследования, так как его сложно читать и рефакторить (для людей).

Преимущества, которые мы получили, используя Python @dataclass

  1. Проверка кода классов теперь занимает примерно половину времени.
  2. В среднем одна строка кода объявления аргумента @dataclass заменяет пятнадцать строк кода.
  3. Неисправный код (ошибки), измеряемый временем создания готового к производству кода, сократился примерно на 8%.

Как объяснить улучшение с помощью Python @dataclass?

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

  • Проверка данных: @dataclass автоматически генерирует методы для проверки достоверности данных в экземпляре класса. @dataclass гарантирует, что данные в экземпляре класса имеют правильный формат и соответствуют всем указанным ограничениям.
  • Улучшенная читаемость: @dataclass улучшает понимание структуры и назначения класса за счет четкого отделения полей данных от поведения.
  • Автоматическое создание стандартного кода: декоратор @dataclass генерирует несколько методов, таких как __init__, __repr__ и __eq__, часто необходимых при работе с классами данных.
  • Улучшенная ремонтопригодность: @dataclass способствует лучшему обслуживанию и снижению стоимости рефакторинга кода за счет четкого разделения данных и поведения.
  • Улучшенная производительность: декоратор @dataclass создает методы, оптимизированные для повышения производительности, такие как метод __init__, который использует аргументы, состоящие только из ключевых слов, для уменьшения количества необходимых поисков атрибутов.
  • Улучшенная совместимость с неизменяемыми структурами данных: значения данных становятся неизменяемыми с помощью параметра @dataclass frozen=TRUE. Мы покажем на примере, как преобразовать набор глобальных констант в общий пакет констант.

Our @dataclass Best Practices

Рекомендация: никогда не используйте «def class» без @dataclass.

Вероятно, есть случаи, когда вы не будете использовать @dataclass с объявлением вашего класса. Не знаю ни одного (дело о наследстве не включаю).

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

Классы могут существовать без состояния и только с методами, но какой в ​​этом смысл?

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

Я пытался вспомнить, создавал ли я когда-нибудь класс без инкапсулированного состояния (члены данных класса). Я не могу думать ни об одном.

Классы существуют для инкапсуляции состояния (полей данных) и методов, которые работают с полями данных. Я преобразовываю методы в функции без инкапсуляции состояния данных.

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

До этой части блога я предполагал, что вы знаете о @dataclass и можете согласиться или не согласиться с моим утверждением:

Никогда не используйте класс def без @dataclass

В следующем разделе вы найдете краткий обзор @dataclass.

Если следующий раздел оставит вас неудовлетворенным, я рекомендую следующее:



Многие другие статьи также дадут вам хорошую основу.

Примечание. Следующее, вероятно, не соответствует действительности, но можете ли вы представить Python версии 4, где @dataclass исчезает, потому что оно объединено с def class.

Примечание. Если я ошибаюсь, возможно, я что-то упустил, дайте мне знать.

Рекомендация: используйте декоратор @dataclass

Вы, наверное, уже подозревали, что я использую @dataclass для всех своих определений классов в Python.

Python @dataclass украшает определение def class и автоматически генерирует три метода двойного дандера __init__(), __repr__() и __eq__().

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



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

Вот пример короткого класса в пакете Photonai, преобразованного с помощью @dataclass:

### Example #1
from inspect import signature, getmembers
from typing import Dict, List, Any
import sys
class Data:
    def __init__(self, X: np.ndarray =None, y: np.array=None,
                 kwargs: Dict =None):
        self.X = X
        self.y = y
        self.kwargs = kwargs
def __repr__(self):
         return self.val
def __eq__(self, other):
         return self.val == other.val

После @dataclass декоратора

from dataclasses import dataclass
@dataclass
class Data:
    X: np.ndarray = None  # The field declaration: X
    y: np.array = None    # The field declaration: y
    kwargs: Dict = None   # The field declaration: kwargs

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

Был ли сгенерирован код__eq__()?

### Example #2

data1 = Data()
data2 = Data()
data1 == data1

Вывод примера #2

True

Да! Как насчет методов __repr__() и __str__?

### Example #3

print(data1)
data1

Пример #3 вывод

Data(X=None, y=None, kwargs=None)
Data(X=None, y=None, kwargs=None)

Да!

Как насчет метода __init__?

Example #4

@dataclass(unsafe_hash=True)
class Data:
    X: np.ndarray = None
    y: np.array = None
    kwargs: Dict = None
        
data3 = Data(1,2,3)

Пример #4 вывод

Data(X=1, y=2, kwargs=3)

Да!

Примечание. Сгенерированный метод __init__ по-прежнему имеет сигнатуру (X, y, kwargs). Также обратите внимание, что интерпретатор Python 3.7 игнорировал подсказки типов.

Вот более длинный пример из photonai/photonai/base/hyperpipe.py:

Пример #5, замена def class декоратором @dataclass

@dataclass
class CrossValidation:
    inner_cv: int
    outer_cv: int
    eval_final_performance: bool = True
    test_size: float = 0.2
    calculate_metrics_per_fold: bool = True
    calculate_metrics_across_folds: bool = False
    outer_folds = None
    inner_folds = dict()

Используя @dataclass вместо def class, я повысил читабельность своего кода.

### Example #6

cv1 = CrossValidation()

Пример #6 вывод

inner_cv и outer_cv являются позиционными аргументами. С любой подписью вы объявляете поле не по умолчанию после поля по умолчанию.

Подсказка: если вышеперечисленное разрешено, наследование от родительского класса прерывается. Это был/является вопросом интервью Goggle.

### Example #7

cv1 = CrossValidation(1,2)
cv2 = CrossValidation(1,2)
cv3 = CrossValidation(3,2,test_size=0.5)
print(cv1)
cv3

Пример #7 вывод

Как насчет методов __eq__?

### Example #8

cv1 == cv2

Пример #8 вывод

True

В примере № 9 эквивалентны ли cv1 и cv3?

### Example #9

cv1 == cv3

Пример #9 вывод

False

Нет, cv1 и cv3 не эквивалентны? Да, метод __eq__ ведет себя корректно.

Явный шаблон @property и @setproperty больше не нужен.

Из всего шаблонного кода @dataclass автоматически устраняет необходимость в @property и @setproperty. Это мое любимое преимущество.

### Example #19

@dataclass
class Data():
 X: np.ndarray = None # The field declaration: X
 y: np.array = None # The field declaration: y
 kwargs: Dict = None # The field declaration: kwargs
 
d = Data()
d.kwargs

Пример #19 вывод

# nothing output

Опять же, с помощью@dataclass, я повысил читабельность своего кода.

Вот как установить kwargs:

### Example #20

d.kwargs = {'one':1}
d.kwargs

Пример #20 вывод

{'one':1}

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

### Example #21

d.kwargs = 1
d.kwargs

Пример #21 вывод

1

Передовой опыт: используйте @dataclass для преобразования набора глобальных констант в общий пакет констант

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

Я должен предупредить тебя. Почти все универсальные константы определены в пакете классов scipy.constants. Пожалуйста, не изобретайте велосипед.

Однако я уверен, что у вас есть любимые константы, которые вы хотели бы использовать в своих пакетах Python. Теперь вы можете обмениваться своими константами с помощью @dataclass вместо утомительного копирования глобальных переменных. Не говоря уже о том, что глобалы приводят к огромным затратам на обслуживание.

Как вы утверждаете, что константы не могут быть изменены?

Если мы хотим создать общий класс констант данных, которые не могут быть удалены (изменены) позже.

Вы создаете класс с неизменяемыми элементами данных с аргументом frozen в dataclass:.

### Example 25

@dataclass(frozen=True)
class Data():
    X: np.ndarray = 0  # The field declaration: X
    y: np.array = 0    # The field declaration: y
    z: int = 0   # The field declaration: kwargs
        
d = Data()
d.y = 2

Пример #25 вывод

Передовой опыт: используйте аргументы @dataclass для управления расширением

Щелчок shift-<tab> в блокноте Jupyter показывает подпись и значение по умолчанию для всех аргументов для @dataclass.

__init__() , __repr__() и __eq__() имеют значение по умолчанию ключевого слова True, а __order__(), __unsafe_hash__(), и __frozen_() имеют значение по умолчанию ключевого слова False.

Вот код:

### Example #17

@dataclass(order = True)
class Data():
    X: np.ndarray = None  # The field declaration: X
    y: np.array = None    # The field declaration: y
    kwargs: Dict = None   # The field declaration: kwargs

Это автоматически сгенерирует следующее:

class Data():
    X: np.ndarray = None  # The field declaration: X
    y: np.array = None    # The field declaration: y
    kwargs: Dict = None   # The field declaration: kwargs
... default autogenerated methods, plus
def __ge__(self, other):
        return self.val >= other.val
    def __gt__(self, other):
        return self.val > other.val
    def __le__(self, other):
        return self.val <= other.val
    def __lt__(self, other):
        return self.val < other.val

Такой, что:

### Example #18

print(data1 > data2)
print(data1 >= data2)
print(data1 < data2)
print(data1 <= data2)

Вывод примера №18:

False
True
False
True

Рекомендация: используйте __slots__ для ускорения доступа

Примените атрибуцию __slots__, если хотите ускорить доступ к элементам данных класса.

### Example #22
@dataclass
class LoggingState:
    __slots__ =  ['debug', 'info', 'success', 'warning', 'error', 'critical']
    debug: bool
    info: bool
    success: bool
    warning: bool
    error: bool
    critical: bool
logg = LoggingState(debug=False, info=False, success=False, warning=True, error=True, critical=True )

Я не использую __slots__, если профиль показывает, что экземпляры класса составляют менее 10% нагрузки.

Примечание. Проверьте производительность для Python 11.x и более поздних версий, поскольку производительность __slots__ может измениться.

Передовой опыт: добавление методов в класс @dataclass

Я добавляю метод power_argsв @dataclass так же, как def class. Вот как это выглядит:

### Example #23

@dataclass
class Data():
    X: np.ndarray = None  # The field declaration: X
    y: np.array = None    # The field declaration: y
    z: int = 0   # The field declaration: kwargs
    
    def power_args(self):
        self.z = self.X**self.y
        
d = Data(1,2)
d.power_args()
d.z

Вывод примера #22:

2

Ага! Способ power_args сработал.

Опять же, вот еще один пример:

### Example #24
        
d = Data(5,2)
d.power_args()
d.z

Вывод примера #23:

25

Опять же, читаемость кода улучшается с помощью @dataclass.

Передовой опыт: использование наследования

@dataclass может быть подклассом другого @dataclass. Например, я расширяю Data() с помощью Datatail @dataclass.

### Example 29

@dataclass
class Data():
    X: np.ndarray = None  
    y: np.array = None    
    kwargs: Dict = None   
        
    def __post_init__(self):
        self.kwargs = {}
        
@dataclass
class Datatail(Data):
    z: int = 0
        
d = Datatail()
d

Пример #29 вывод

Передовой опыт: используйте обработку Post-Init

Существует метод post-init, который является частью определения @dataclass. Метод __post_init__ запускается после __init__, сгенерированного @dataclass.. Он позволяет выполнять обработку после установки состояния подписи.

Если вы попытаетесь установить для списка, кортежа или Dict что-либо, кроме None, это приведет к ошибке.

### Example 26

@dataclass
class Data():
 X: np.ndarray = None # The field declaration: X
 y: np.array = None # The field declaration: y
 kwargs: Dict = {} # The field declaration: kwargs

Вывод примера #26:

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

Точно так же мы можем решить проблему инициации, с которой столкнулись в конце метода 2. Проверка шаблонного кода класса @dataclass Generation of def.

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

### Example 28

from dataclasses import dataclass
@dataclass
class CrossValidation:
    inner_cv: int = 0
    outer_cv: int = 0
    eval_final_performance: bool = True
    test_size: float = 0.2
    calculate_metrics_per_fold: bool = True
    calculate_metrics_across_folds: bool = False
def __post_init__(self):
        self.outer_folds = dict()
        self.inner_folds = dict()

Примечание. Типовые подсказки можно использовать в __post_init__ ..

Пример #28 вывод

Передовой опыт: проверка создания @dataclass шаблонного шаблона класса def

Вызывая help, мы детализируем элементы данных и методы @dataclass Data..

### Example #10

help(Data)

Пример #10 вывод

На рисунке 5 мы видим, что @dataclass автоматически сгенерировал три метода двойного дандера: __init__(), __repr__() и __eq__().

Функция getmembershelpдетализирует метаданные каждого члена (атрибута) класса Data.

### Example #11

getmembers(Data)

Пример #11 вывод

Мы смотрим в класс, используя библиотеку inspect. Проверяем метод data1 с помощью signature:

### Example #12

from inspect import signature
print(signature(data1.__init__))

### Example #13

print(signature(data1.__eq__))

Пример #13 вывод

(other)

Майор крутой!

### Example #14

print(signature(cv1.__init__))

Пример #14 вывод

Упс! А как насчет этого сценария?

# Example #15

@dataclass
class CrossValidation:
    inner_cv: int
    outer_cv: int
    eval_final_performance: bool = True
    test_size: float = 0.2
    calculate_metrics_per_fold: bool = True
    calculate_metrics_across_folds: bool = False
    outer_folds = None
    inner_folds = dict()
cv1 = CrossValidation(1,2)
cv2 = CrossValidation(1,2)
cv3 = CrossValidation(3,2,test_size=0.5)
print(cv1)
cv3

Пример #15 вывод

outer_folds и inner_folds являются элементами данных, но не создаются в сигнатуре вызова.

Почему нет? Я только заметил, что не дал подсказку типа.

Подождите минуту; это не может быть причиной. Тип интерпретатора Python игнорирует подсказки типа. Может ли это быть пропущенным угловым случаем теста?

Возможно, более поздние версии Python изменят это @dataclass поведение.

Не волнуйтесь, @dataclass справляется с этим вариантом использования. Я показываю, как с помощью техники 8. Пост-инициальная обработка.

Краткое содержание

Мы приветствовали @dataclass в Python 3.7 не из-за какой-либо функциональности, а потому, что он автоматически генерировал большую часть шаблонов, необходимых для определения класса (объекта) с состоянием данных. Второй близкой причиной была улучшенная читабельность нашего кода Python.

Я продемонстрировал это на двадцати девяти примерах кода, которые показывают, как @dataclass преобразовывает классы. Мы видели, как @dataclass значительно повысил читабельность.

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

Продолжайте продуктивно программировать! Продолжайте веселиться!

Ресурсы

  1. Классы данных в Python 3.7+ (Руководство)
  2. Документация Python @dataclass