Статьи

Глубокое погружение в декораторы Python

Декораторы Python — одна из моих любимых возможностей Python. Они являются наиболее дружественной * и * удобной для разработчиков реализацией аспектно-ориентированного программирования, которую я видел на любом языке программирования.

Декоратор позволяет дополнять, изменять или полностью заменять логику функции или метода. Это сухое описание не делает декораторов справедливости. Как только вы начнете использовать их, вы откроете для себя целый мир аккуратных приложений, которые помогают сохранить ваш код трудным и чистым и переместить важные «административные» задачи из основного потока вашего кода в декоратор.

Прежде чем мы перейдем к некоторым классным примерам, если вы хотите немного подробнее изучить происхождение декораторов, то в Python 2.4 впервые появились функциональные декораторы. См. PEP-0318 для интересного обсуждения истории, обоснования и выбора имени «декоратор». Декораторы классов впервые появились в Python 3.0. См. PEP-3129 , который довольно короткий и основывается на всех концепциях и идеях декораторов функций.

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

Классическими примерами являются встроенные декораторы @staticmethod и @classmethod. Эти декораторы превращают метод класса соответственно в статический метод (сам первый аргумент не предоставляется) или в метод класса (первый аргумент — это класс, а не экземпляр).

01
02
03
04
05
06
07
08
09
10
11
12
class A(object):
    @classmethod
    def foo(cls):
        print cls.__name__
 
    @staticmethod
    def bar():
        print ‘I have no use for the instance or class’
         
        
A.foo()
A.bar()

Выход:

1
2
A
I have no use for the instance or class

Статические и классовые методы полезны, когда у вас нет экземпляра под рукой. Они часто используются, и было очень сложно применять их без синтаксиса декоратора.

Декоратор @memoize запоминает результат первого вызова функции для определенного набора параметров и кэширует его. Последующие вызовы с теми же параметрами возвращают кешированный результат.

Это может значительно повысить производительность для функций, которые выполняют дорогостоящую обработку (например, обращаются к удаленной базе данных или вызывают несколько API REST) ​​и часто вызываются с одинаковыми параметрами.

1
2
3
4
5
@memoize
def fetch_data(items):
    «»»Do some serious work here»»»
    result = [fetch_item_data(i) for i in items]
    return result

Как насчет пары декораторов, называемых @precondition и @postcondition, для проверки входного аргумента, а также результата? Рассмотрим следующую простую функцию:

1
2
3
def add_small ints(a, b):
   «»»Add two ints whose sum is still an int»»»
   return a + b

Если кто-то вызывает его с большими целыми числами, длинными или даже строковыми значениями, это будет успешно выполнено, но это нарушит договор о том, что результатом должно быть int. Если кто-то вызывает его с несовпадающими типами данных, вы получите общую ошибку времени выполнения. Вы можете добавить следующий код в функцию:

1
2
3
4
5
6
7
8
def add_small ints(a, b):
   «»»Add two ints in the whose sum is still an int»»»
   assert(isinstance(a, int), ‘a must be an int’)
   assert(isinstance(a, int), ‘b must be an int’)
   result = a + b
   assert(isinstance(result, int),
          ‘the arguments are too big.
   return result

Наша милая однострочная add_small_ints() превратилась в неприятное болото с add_small_ints() . В реальной функции может быть очень сложно сразу увидеть, что она на самом деле делает. С помощью декораторов предварительные и последующие условия могут выходить из тела функции:

1
2
3
4
5
6
7
@precondition(isinstance(a, int), ‘a must be an int’)
@precondition(isinstance(b, int), ‘b must be an int’)
@postcondition(isinstance(retval, int),
               ‘the arguments are too big.
def add_small ints(a, b):
    «»»Add two ints in the whose sum is still an int»»»
    return a + b

Предположим, у вас есть класс, который требует авторизации через секрет для всех его многочисленных методов. Будучи непревзойденным разработчиком Python, вы, вероятно, выбрали бы декоратор метода @authorized, как в:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class SuperSecret(object):
   @authorized
   def f_1(*args, secret):
       «»» «»»
        
   @authorized
   def f_2(*args, secret):
       «»» «»»
   .
   .
   .
   @authorized
   def f_100(*args, secret):
       «»» «»»

Это, безусловно, хороший подход, но это немного раздражает повторять это, особенно если у вас много таких классов.

Что более важно, если кто-то добавляет новый метод и забывает добавить украшение @authorized, у вас есть проблема с безопасностью. Не бойся Декораторы класса Python 3 получили вашу поддержку. Следующий синтаксис позволит вам (с правильным определением декоратора класса) автоматически авторизовать каждый метод целевых классов:

01
02
03
04
05
06
07
08
09
10
11
12
@authorized
class SuperSecret(object):
    def f_1(*args, secret):
        «»» «»»
         
    def f_2(*args, secret):
        «»» «»»
    .
    .
    .
    def f_100(*args, secret):
        «»» «»»

Все, что вам нужно сделать, это украсить сам класс. Обратите внимание, что декоратор может быть умным и игнорировать специальный метод, такой как __init __ (), или может быть настроен для применения к определенному подмножеству при необходимости. Небо (или ваше воображение) это предел.

Если вы хотите продолжить дальнейшие примеры, проверьте PythonDecoratorLibrary .

Теперь, когда вы видели несколько примеров в действии, пришло время раскрыть магию. Формальное определение состоит в том, что декоратор является вызываемым объектом, который принимает вызываемый элемент (цель) и возвращает вызываемый элемент (оформленный), который принимает те же аргументы, что и исходная цель.

Ого! это много слов непонятно друг на друга. Во-первых, что называется? Вызываемый объект — это просто объект Python, имеющий метод __call __ () . Как правило, это функции, методы и классы, но вы можете реализовать метод __call __ () в одном из ваших классов, и тогда ваши экземпляры классов также станут вызываемыми. Чтобы проверить, может ли объект Python вызываться, вы можете использовать встроенную функцию callable ():

1
2
3
4
5
callable(len)
True
 
callable(‘123’)
False

Обратите внимание, что функция callable () была удалена из Python 3.0 и возвращена в Python 3.2, поэтому, если по какой-то причине вы используете Python 3.0 или 3.1, вам придется проверить наличие атрибута __call__, как в hasattr(len, '__call__') .

Когда вы берете такой декоратор и применяете его с использованием синтаксиса @ к некоторому вызываемому элементу, исходный вызываемый элемент заменяется вызываемым элементом, возвращаемым декоратором. Это может быть немного трудно понять, поэтому давайте проиллюстрируем это, посмотрев на интуицию некоторых простых декораторов.

Декоратор функций — это декоратор, который используется для декорирования функции или метода. Предположим, мы хотим напечатать строку «Да, это работает!» каждый раз, когда вызывается декорированная функция или метод, прежде чем вызывать исходную функцию. Вот не-декораторский способ добиться этого. Вот функция foo (), которая выводит здесь «foo ()»:

1
2
3
4
5
6
7
8
def foo():
    print ‘foo() here’
 
foo()
 
Output:
 
foo() here

Вот уродливый способ достичь желаемого результата:

01
02
03
04
05
06
07
08
09
10
11
12
13
original_foo = foo
 
def decorated_foo():
    print ‘Yeah, it works!’
    original_foo()
 
foo = decorated_foo
foo()
 
Output:
 
Yeah, it works!
foo() here

Есть несколько проблем с этим подходом:

  • Это много работы.
  • Вы загрязняете пространство имен промежуточными именами, такими как original_foo () и decor_foo () .
  • Вы должны повторить это для каждой другой функции, которую вы хотите украсить с той же возможностью.

Декоратор, который достигает того же результата, а также может использоваться повторно и может быть скомпонован так:

1
2
3
4
5
def yeah_it_works(f):
   def decorated(*args, **kwargs):
       print ‘Yeah, it works’
       return f(*args, **kwargs)
  return decorated

Обратите внимание, что yeah_it_works () — это функция (следовательно, вызываемая), которая принимает вызываемый элемент ** f ** в качестве аргумента и возвращает вызываемый элемент (вложенная функция ** украшенная **), которая принимает любое количество и типы аргументов.

Теперь мы можем применить его к любой функции:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@yeah_it_works
def f1()
    print ‘f1() here’
 
@yeah_it_works
def f2()
    print ‘f3() here’
 
@yeah_it_works
def f3()
    print ‘f3() here’
 
f1()
f2()
f3()
 
 
Output:
 
 
Yeah, it works
f1() here
Yeah, it works
f2() here
Yeah, it works
f3() here

Как это работает? Оригинальные функции f1 , f2 и f3 были заменены декорированной вложенной функцией, возвращаемой yeah_it_works . Для каждой отдельной функции захваченная функция f является исходной функцией ( f1 , f2 или f3 ), поэтому декорированная функция отличается и выполняет правильные действия, что выдает «Да, это работает!» а затем вызвать исходную функцию f .

Классовые декораторы работают на более высоком уровне и украшают целый класс. Их эффект имеет место во время определения класса. Вы можете использовать их для добавления или удаления методов любого декорированного класса или даже для применения декораторов функций ко всему набору методов.

Предположим, мы хотим отслеживать все исключения, вызванные из определенного класса в атрибуте класса. Давайте предположим, что у нас уже есть декоратор функций track_exceptions_decorator, который выполняет эту функцию. Без декоратора класса вы можете вручную применить его к каждому методу или прибегнуть к метаклассам . Например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class A(object):
   @track_exceptions_decorator
   def f1():
       …
        
   @track_exceptions_decorator
   def f2():
       …
   .
   .
   .
   @track_exceptions_decorator
   def f100():
       …

Декоратор класса, который достигает того же результата:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
def track_exception(cls):
    # Get all callable attributes of the class
    callable_attributes = {k:v for k, v in cls.__dict__.items()
                           if callable(v)}
    # Decorate each callable attribute of to the input class
    for name, func in callable_attributes.items():
        decorated = track_exceptions_decorator(func)
        setattr(cls, name, decorated)
    return cls
 
@track_exceptions
class A:
    def f1(self):
        print(‘1’)
     
    def f2(self):
        print(‘2’)

Python хорошо известен своей гибкостью. Декораторы переходят на следующий уровень. Вы можете упаковать сквозные задачи в повторно используемые декораторы и применить их к функциям, методам и целым классам. Я настоятельно рекомендую каждому серьезному разработчику Python ознакомиться с декораторами и в полной мере воспользоваться их преимуществами.