В рамках курса «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()».