[Перевод] 5 советов по использованию декораторов в Python

Хочешь писать лаконичный, читаемый и эффективный код? Тогда декораторы помогут тебе в этом.

В седьмой глав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 — Создавайте пользовательские декораторы

Совет

Пишите свои собственные декораторы, чтобы улучшить свой рабочий процесс, но будьте осторожны!

Область видимости в декораторах немного странная. У нас нет времени на детали, но есть эта статья. Примите во внимание, что если вы получаете эту ошибку, то вам следует почитать про область видимости декораторов:

image-loader.svg

Перейдем к некоторым пользовательским декораторам.

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 для каждой функции.

И это всего лишь несколько примеров. Декораторы действительно могут быть очень полезными!

© Habrahabr.ru