Хочешь писать лаконичный, читаемый и эффективный код? Тогда декораторы помогут тебе в этом.
В седьмой главe "Fluent Python" Лучано Ромальо рассказывает о декораторах и замыкании. Они не очень распространены в Data Science, однако как только вы начинаете проектировать модели и писать ассинхронный код, декораторы становятся бесценными помощниками.
1 - Что такое декораторы?
Перед тем как мы перейдем к советам, давайте рассмотрим работу декораторов.
Декораторы - это простые функции, которые принимают на вход функцию. Чаще всего они изображаются как "@my_decorator"
над декорируемой функцией.
temp = 0
def decorator_1(func):
print('Running our function')
return func
@decorator_1
def temperature():
return temp
print(temperature())
Однако то, как мы вызываем функцию temperature()
может сбить с толку. Нужно просто использоватьdecorator_1(temperature())
,как в примере ниже:
temp = 0
def decorator_1(func):
print('Running our function')
return func
def temperature():
return temp
decorator_1(temperature())
Хорошо, значит декораторы - это функции, которые принимают другую функцию в качестве аргументы. Но зачем нам вообще нужно когда-либо их использовать?
Декораторы, действительно, универсальные и мощные инструменты. Они широко применяются для асинхронных функций обратного вызова и функционального программирования. Декораторы могут также использоваться для превращения class-like функциональности в непосредственные функции, сокращая при этом время разработки и заполнение памяти.
2 - Декоратор свойств
Совет
Используйте встроенный декоратор@property
для расширения функциональности геттеров и сеттеров.
Одним из самых используемых встроенных декораторов является @property
. Множетсво ООП языков(Java, С++) предоставляют возможность использовать геттеры и сеттеры. Данные функции используются с целью гарантировать, что наша переменная не вернет/установит некорректное значение.Одним из примеров может служить наша переменная temp, которая по условию должна быть больше нуля.
class my_vars:
def __init__(self, t):
self._temp = t
def my_getter(self):
return self._temp
def my_setter(self, t):
if t > −273.15:
self._temp = t
else:
print('Below absolute 0!')
v = my_vars(500)
print(v.my_getter()) # 500
v.my_setter(-1000) # 'Below absolute 0!'
v.my_setter(-270)
print(v.my_getter()) # -270
Мы можем расширить функциональность многих вещей, используя @property
, делая при этом код чище и динамичнее:
class my_vars:
def __init__(self, t):
self._temp = t
@property
def temperature(self):
return self._temp
@temperature.setter
def temperature(self, t):
self._temp = t
c = my_vars(500)
print(c.temperature) # 500
c.temperature = 1
print(c.temperature) # 1
Заметьте, что мы удалили все условные операторы из my_setter() для краткости, но смысл остался тот же.
Перед тем как мы двинемся дальше, есть еще одно уточнение. В python не существует такого понятия, как "приватные переменные". Префикс "_" указывает на то, что переменная защищена и на нее не стоит ссылаться вне класса. Однако вы все еще можете сделать так:
c = my_vars(500)
print(c._temp) # 500
c._temp = -10000
print(c._temp) # -1000
Отсутствие приватных переменных в Python являлось интересной дизайнерской задумкой. Аргументы - это приватные переменные в ООП, которые на самом деле не является таковыми: если кто-то захочет получить к ним доступ, то он может изменить источник кода класса и сделать переменную публичной.
Python поощряет "ответственную разработку" и позволяет вам получить извне доступ ко всему в классе.
3 - Статические методы и методы классов
Совет
Используйте@classmethodи @staticmethodдля расширения функциональности классов
Эти два декоратора многих сбивают с толку, но их отличия налицо:
@classmethod
принимает класс в качестве параметра. По этой причине методы классов могут модифицировать сам класс через все его экземпляры.@staticmethod
принимает экземпляр класса. По этой причине статические методы вовсе не могут модифицировать классы.
Обратимся к примеру:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def fromBirthYear(cls, name, year):
return cls(name, date.today().year - year)
@staticmethod
def isAdult(age):
return age > 18
Самый важный фактор, определяющий значимость методов класса, это их способность служить альтернативным конструктором для наших классов, которые действительно полезны для полиморфизма. Даже если вы не делаете всякие безумные вещи с наследованием, то все еще прекрасно иметь возможность конкретизировать различные версии классов без использования if/else.
С другой стороны, статические методы чаще всего используются в качестве вспомогательных функций, которые абсолютно независимы от состояния класса. Заметьте, что функция isAdult(age) не требует привычного self аргумента, так что она не может сослаться на класс, даже если очень захочется.
4 - Быстрый совет
Совет
Используйте @functools.wraps
, чтобы хранить информацию функции.
Запомните, декораторы - это просто функции, которые принимают другие функции. Так что, когда мы вызываем "декорированные" функции, в первую очередь мы вызываем сам декоратор.Этот поток перезаписывает информацию о "декорированной" функции, например, __name__
и __doc__
поля.
Чтобы решить эту проблему, мы можем обратиться к следующему декоратору:
from functools import wraps
def my_decorator(func):
@wraps(func)
def call_func(*args):
return func(*args)
return call_func
@my_decorator
def f(x):
"""does some math"""
return x + x * x
print(f(5)) # 30
print(f.__name__) # 'f'
print(f.__doc__) # 'does some math'
Без декоратора @wraps результат напечатанного утверждения будет следующим:
print(f(5)) # 30
print(f.__name__) # 'call_func'
print(f.__doc__) # '
Чтобы избежать переписывания важной информации, убедитесь в использовании @functools.wraps
5 - Создавайте пользовательские декораторы
Совет
Пишите свои собственные декораторы, чтобы улучшить свой рабочий процесс, но будьте осторожны!
Область видимости в декораторах немного странная. У нас нет времени на детали, но есть эта статья. Примите во внимание, что если вы получаете эту ошибку, то вам следует почитать про область видимости декораторов:
Перейдем к некоторым пользовательским декораторам.
5.1 - Сохраняем функции, основанные на декораторах
Код ниже добавляет функции в список при вызове
# Desc: store all ml models and call them
ml_models = []
def ml(func):
ml_models.append(func)
def call_func(*args, **kwargs):
return func(*args, **kwargs)
return call_func
@ml
def CNN():
print('Convolutional Neural Net')
@ml
def RNN():
print('Recurrent Neural Net')
def linear_regression():
print('This isn't ML')
# call all ML models
for m in ml_models:
m()
print(ml_models) # returns list of functions for reference
Потенциальный пример использования - юнит-тестирование, так же как с pytest. Условно, мы имеем быстрые и медленные тесты. Вместо того, чтобы вручную назначать каждый отдельному списку, мы можем просто добавить@slowили @fastдекораторы для каждой функции, а затем вызвать каждое значение в соответсвующем списке.
5.2 - Запросы временных данных и модельное обучение
Код ниже выводит время исполнения вашей функции
# Desc: create a decorator that prints start/end time of function
import numpy as np
def time_it(func):
def timer(*args):
start = np.datetime64('now')
print(start)
result = func(*args)
end = np.datetime64('now')
print(end)
print(f'{(end - start) / np.timedelta64(1, "s")} secs')
return result
return timer
@time_it
def long_function(x):
for i in range(x):
_ = i * i + 5
long_function(int(1e6))
"""
Output:
2022-01-24T02:37:15
2.0 secs
2022-01-24T02:39:15
"""
Если вы запускаете любой тип запроса данных или обучаете модель с плохими логами, полезно иметь оценку время исполнения программы. Вы можете оценить время исполнения любой функции, пользуясь декоратором @time_it
.
5.3 - Выполнять управление потоком на входе функции
Приведенный ниже код выполняет условные проверки параметров функции перед выполнением самой функции.
def check_not_None(func):
def check(x):
if x is not None:
return func(x)
else:
return 'is None'
return check
@check_not_None
def f1(x):
return x**1
@check_not_None
def f2(x):
return x**2
@check_not_None
def f3(x):
return x**3
print(f1(4)) # 4
print(f2(None)) # 'is None'
print(f3(4)) # 64
Этот декоратор применяет логику условий на все параметры x функции. Без декоратора нам бы пришлось писать if is not None
для каждой функции.
И это всего лишь несколько примеров. Декораторы действительно могут быть очень полезными!