В рамках курса «Python Developer. Basic» подготовили для вас перевод полезного материала.

Также приглашаем всех желающих на
открытый вебинар по теме «Три кита: map(), filter() и zip()». Можно ли писать код, требующий циклов, но без циклов? Можно. Может ли он быть быстрее, чем, если бы мы использовали циклы в Python? Может. Для реализации задуманного понадобится знание слов "callback", "iterator" и "lambda". Будет сложно, но интересно. Присоединяйтесь.


Мы добавляем алгоритмы кластеризации с помощью пакетов scikit-learn, Keras и других в пакет Photonai. На 12 примерах мы покажем, как @dataclass улучшает код на Python. Для этого мы используем код из пакета Photonai для Machine Learning.

Обновитесь до Python 3.7 или более поздней версии

Декоратор @dataclass был добавлен в Python 3.7. Можно использовать Python 3.7 из Docker-образа, добавив в файл следующие команды /.bashrc_profile или /bashrc.txt.

devdir='<path-to-projects>/photon/photonai/dockerSeasons/dev/'
testdir='<path-to-projects>/photon/photonai/dockerSeasons/test/'
echo $devdir
echo $testdir
export testdir
export devdir
#
alias updev="(cd $devdir; docker-compose up) &"
alias downdev="(cd $devdir; docker-compose down) &"
alias builddev="(cd $devdir; docker-compose build) &"
#
alias uptest="(cd $testdir; docker-compose up) & "
alias downtest="(cd $testdir; docker-compose down) &"
alias buildtest="cd $testdir; docker-compose build) &"

Если вы не можете найти файл /bashrc.txt создайте его самостоятельно с помощью touch/bashrc.txt. (в случае MacOS или одной из разновидностей операционных систем Linux или Unix.)

Примечание: Не забудьте указать в качестве исходника ?/.bashrc_profile или ?/bashrc.txt, когда закончите их редактировать. 

Здесь вы найдете более подробную информацию о реализации Docker, которой я пользуюсь. 

Примечание: вы можете добавить код Docker в свой проект из клонируемого репозитория на GitHub.

Добавьте подсказки типов

Python – язык с динамической типизацией. В версиях Python от 3.5 есть подсказки типов (PEP 484). Я подчеркиваю, что именно подсказки, поскольку они не влияют на работу интерпретатора Python. Насколько вам известно, интерпретатор Python вообще их игнорирует.

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

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

Я добавляю подсказки типов во все приведенные примеры @dataclass. Если вы хотите узнать о них больше, рекомендую почитать:

  1. https://medium.com/swlh/future-proof-your-python-code-20ef2b75e9f5

  2. https://realpython.com/python-type-checking/

  3. https://docs.python.org/3/library/typing.html

Декоратор @dataclass уменьшает шаблонность

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

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

Примечание: Если вы не используете pandas, можно ускорить выполнение этих функции, с помощью быстрой вставки @jit из пакета numba.

@dataclass декорирует определение класса def и автоматически генерирует 5 методов init(), repr(), str, eq(), и hash().

Примечание: он генерирует и другие методы, но об этом позже.

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

Пример коротенького класса в photon/photonai/base/hyperpipe.py, декорированный с помощью @dataclass.

### Example #1

class Data:
    def __init__(self, X=None, y=None, kwargs=None):
        self.X = X
        self.y = y
        self.kwargs = kwargs

Пример 1, после декорации =>

from dataclasses import dataclass
from typing import Dict
import numpy as np
@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

Пример , вывод =>

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

Да! А методы hash() и 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)
{data3:1}

Пример 4, вывод =>

{Data(X=1, y=2, kwargs=3): 1}

Да!

Примечание: У сгенерированного метода init все еще сигнатура (X, y, kwargs). Кроме того, обратите внимание, что подсказки типов были проигнорированы интерпретатором Python 3.7.

Примечание: У init(), repr(), str и eq() значение ключевого слова по умолчанию True, тогда как у hash() по умолчанию False.

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

### Example #5

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

Пример 5, вывод =>

(X: numpy.ndarray = None, y: <built-in function array> = None, 
kwargs: Dict = None) -> None

Круто!

Более длинный пример из photon/photonai/base/hyperpipe.py

### Example #6

class CrossValidation:

    def __init__(self, inner_cv, outer_cv,
                 eval_final_performance, test_size,
                 calculate_metrics_per_fold,
                 calculate_metrics_across_folds):
        self.inner_cv = inner_cv
        self.outer_cv = outer_cv
        self.eval_final_performance = eval_final_performance
        self.test_size = test_size
        self.calculate_metrics_per_fold = calculate_metrics_per_fold
        self.calculate_metrics_across_folds =
            calculate_metrics_across_folds

        self.outer_folds = None
        self.inner_folds = dict()Example #6 Output=>

Пример 6, после декорации =>

from dataclasses import 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
Note:(Example #6) As any signature, keyword arguments fields with default values must be declared last.
Note:(Example #6)  class CrossValidation: Readability has increased substantially by using @dataclass and type hinting.
### Example #7
cv1 = CrossValidation()

Пример 7, вывод =>

TypeError: __init__() missing 2 required positional arguments: 'inner_cv' and 'outer_cv'
Note:(Example #7) inner_cv and outer_cv are positional arguments. With any signature, you declare a non-default field after a default one. (Hint: If this were allowed, inheritance from a parent class breaks.)((Why? Goggle interview question #666.))
### Example #8
cv1 = CrossValidation(1,2)
cv2 = CrossValidation(1,2)
cv3 = CrossValidation(3,2,test_size=0.5)
print(cv1)
cv3

Пример 8, вывод =>

CrossValidation(inner_cv=1, outer_cv=2, eval_final_performance=True, test_size=0.2, calculate_metrics_per_fold=True, calculate_metrics_across_folds=False)
CrossValidation(inner_cv=3, outer_cv=2, eval_final_performance=True, test_size=0.5, calculate_metrics_per_fold=True, calculate_metrics_across_folds=False)
### Example #9
cv1 == cv2

Пример 9, вывод =>

True
### Example #10

cv1 == cv3

Пример 10, вывод =>

False
### Example #11
from inspect import signature
print(signature(cv3.__init__))
cv3

Пример 11, вывод =>

(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) -> None
CrossValidation(inner_cv=3, outer_cv=2, eval_final_performance=True, test_size=0.5, calculate_metrics_per_fold=True, calculate_metrics_across_folds=False)
Note: (Example #11) The inspect function shows the signature of the class object while the__str__ default shows the instance state variables and their values.

Очень круто!

Упс, а что насчет:

self.outer_folds = None
self.inner_folds = dict()

У нас есть переменные состояния, но они не создаются при вызове. Не волнуйтесь, @dataclass справится и с этим. Покажу в следующем разделе.

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

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

Мы завершаем преобразование установив оставшееся состояние CrossValidation:

### Example 12
from dataclasses import 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
    def __post_init__(self):
        self.outer_folds = None
        self.inner_folds = dict()

Источники

Здесь вы найдете отличные варианты использования декоратора @dataclass:

  1. https://realpython.com/python-data-classes/

  2. https://blog.usejournal.com/new-buzzword-in-python-is-here-dataclasses-843dd1d372a5

Заключение

На 12 примерах «до и после» я показал, как @dataclass преобразует классы в пакете Photonai Machine Learning. Мы видели, как @dataclass повысил производительность и читаемость кода. 

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

Добавление @dataclass и подсказок типов демонстрирует, что Python продолжает расти и развиваться.

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

Я показал далеко не все возможности @dataclass. Поскольку мы только добавляем кластеризацию, я продолжу документировать изменения в photonai.


Узнать подробнее о курсе «Python Developer. Basic».

Смотреть открытый вебинар по теме «Три кита: map(), filter() и zip()».