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

Совет

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

Область видимости в декораторах немного странная. У нас нет времени на детали, но есть эта <a href=»https://towardsdatascience.com/closures-and-decorators-in-python-2551abbc6eb6«>статья</a>. Примите во внимание, что если вы получаете эту ошибку, то вам следует почитать про область видимости декораторов:

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

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

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


ссылка на оригинал статьи https://habr.com/ru/post/648967/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *