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

В седьмой глав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 для каждой функции.

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

Комментарии (7)


  1. DeepFakescovery
    01.02.2022 12:57
    -3

    property - это неочевидное поведение для программиста, использующего класс.

    c.temperature = 1

    выглядит как прямая перезапись атрибута 'temperature' объектом int(1)

    На деле же при ассигновании выполняется скрытая логика, о чем можно узнать только если перейти в реализацию класса и запомнить этот факт именно с этим свойством. Читаемость кода как раз падает, из-за того что появляются дыры в понимании, что конкретно делает программа.

    А вот get/set это очевидные команды объекту с очевидной логикой.

    Я бы вообще удалил properties из питона.


    1. mayorovp
      01.02.2022 19:42

      Присваивание c.temperature = 1 выглядит как присвоение 1 свойству temperature объекта c. То, что происходит какая-то перезапись атрибута — это ваши додумки, которые противоречат как реализации объектов в Питоне, так и общему принципу инкапсуляции.


      1. vladis005 Автор
        01.02.2022 23:20
        -1

        Не мои, а автора????


  1. longclaps
    01.02.2022 13:08

    На проверку в статье 4 совета. От себя добавлю пятый, для автора: не привирай.


  1. dvserg
    01.02.2022 13:32
    +1

    Одним из самых используемых встроенных декораторов является @property. Множетсво ООП языков(Java, С++) предоставляют возможность использовать геттеры и сеттеры. Данные функции используются с целью гарантировать, что наша переменная не вернет/установит некорректное значение.

    Для С++ в том виде, в котором это подразумевается в статье - это не верно. Путаница с Borland C++Builder


  1. Pavel1114
    02.02.2022 05:38
    +3

    temp = 0
    def decorator_1(func):
      print('Running our function')
      return func
    def temperature():
      return temp
    decorator_1(temperature())
    



    Где здесь декоратор? Ниже вы приводите определение декоратора как функции, которая в качестве аргумента принимает другую функцию. Это конечно не всё определение. Но этот пример даже данное условие не выполняет.
    ps: обращаюсь к вам так как принимая решение о переводе вы также берёте на себя часть ответственности за содержание
    pss: не смотря на то что я уважаю чужой труд, считаю бессмысленные статьи(и их переводы) только создают информационный шум


    1. Pavel1114
      02.02.2022 09:52
      +1

      Вспомнил, тут на хабре был небольшой, но очень подробный цикл о декораторах в python
      Шаг 1. Базовое понимание декораторов
      Шаг 2. Продвинутое использование декораторов