Изучение преимуществ @dataclass
Почему @dataclass?
Моим первым языком был FORTRAN, который более или менее непригоден для распределенных вычислений из-за глобальных переменных. Тогда я поклялся избегать глобальных переменных, если смогу.
Я выбрал объектно-ориентированный (LOOPS — Lisp Library) из-за его способности инкапсулировать состояния данных. Передача всех инкапсулированных данных была выполнена путем передачи экземпляра объекта (в памяти).
В течение тридцати лет я использовал объекты или классы на каждом языке. Я стесняюсь множественного наследования, так как его сложно читать и рефакторить (для людей).
Преимущества, которые мы получили, используя Python @dataclass
- Проверка кода классов теперь занимает примерно половину времени.
- В среднем одна строка кода объявления аргумента
@dataclass
заменяет пятнадцать строк кода. - Неисправный код (ошибки), измеряемый временем создания готового к производству кода, сократился примерно на 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
значительно повысил читабельность.
Лучшая читабельность облегчает понимание живого производственного кода для всех, включая программиста. Вы лучше понимаете результаты, что приводит к лучшему тестированию, меньшему количеству ошибок и меньшим затратам на обслуживание.
Продолжайте продуктивно программировать! Продолжайте веселиться!
Ресурсы
- Классы данных в Python 3.7+ (Руководство)
- Документация Python @dataclass