Статьи

Закрытие Python и декораторы (Часть 2)

Изменить: получил жалобы на то, что код было трудно читать, пробуя Pygments .

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

    System Message: ERROR/3 (<string>, line 11)

    Unknown directive type "code".

    .. code:: python
     >>> def print_call(fn):
     ...   def fn_wrap(*args, **kwargs):
     ...     print("Calling %s with arguments: \n\targs: %s\n\tkwargs:%s" % (
     ...            fn.__name__, args, kwargs))
     ...     retval = fn(*args, **kwargs)
     ...     print("%s returning '%s'" % (fn.func_name, retval))
     ...     return retval
     ...   fn_wrap.func_name = fn.func_name
     ...   return fn_wrap
     ...
     >>> def greeter(greeting, what='world'):
     ...     return "%s %s!" % (greeting, what)
     ...
     >>> greeter = print_call(greeter)
     >>> greeter("Hi")
     Calling greeter with arguments:
          args: ('Hi',)
          kwargs:{}
     greeter returning 'Hi world!'
     'Hi world!'
     >>> greeter("Hi", what="Python")
     Calling greeter with arguments:
          args: ('Hi',)
          kwargs:{'what': 'Python'}
     greeter returning 'Hi Python!'
     'Hi Python!'
     >>>

Так что это, по крайней мере, немного полезно, но будет лучше! Возможно , вы слышали или не слышали о замыканиях , и, возможно, вы слышали какое-то большое количество определений того, что такое замыкание — я не буду вдаваться в придирки, а просто скажу, что замыкание — это блок кода (например, функция), которая захватывает (или закрывает ) нелокальные (свободные) переменные. Если это все для вас бессмысленно, вам, вероятно, нужен освежитель CS, но не бойтесь — я покажу на примере, и концепция достаточно проста для понимания: функция может ссылаться на переменные, которые определены в функции объем ограждения.

Например, взгляните на этот код:

    System Message: ERROR/3 (<string>, line 54)

    Unknown directive type "code".

    .. code:: python
     >>> a = 0
     >>> def get_a():
     ...   return a
     ...
     >>> get_a()
     0
     >>> a = 3
     >>> get_a()
     3

Как видите, функция get_a может получить значение a и сможет прочитать обновленное значение. Тем не менее, существует ограничение — записанная переменная не может быть записана в:

    System Message: ERROR/3 (<string>, line 70)

    Unknown directive type "code".

    .. code:: python
     >>> def set_a(val):
     ...   a = val
     ...
     >>> set_a(4)
     >>> a
     3

Что здесь случилось? Поскольку замыкание не может записывать в любые захваченные переменные, a = val фактически записывает в локальную переменную a, которая скрывает уровень модуля a, в который мы хотели записать. Чтобы обойти это ограничение (которое может или не может быть хорошей идеей), мы можем использовать тип контейнера:

    System Message: ERROR/3 (<string>, line 85)

    Unknown directive type "code".

    .. code:: python
     >>> class A(object): pass
     ...
     >>> a = A()
     >>> a.value = 1
     >>> def set_a(val):
     ...   a.value = val
     ...
     >>> a.value
     1
     >>> set_a(5)
     >>> a.value
     5

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

    System Message: ERROR/3 (<string>, line 110)

    Unknown directive type "code".

    .. code:: python
     >>> #Our 'backend' function
     ... def get_stuff(user, pw, stuff_id):
     ...   """Here we would presumably fetch data using the supplied
     ...   credentials and id"""
     ...   print("get_stuff called with user: %s, pw: %s, stuff_id: %s" % (
     ...         user, pw, stuff_id))
     >>> def partial(fn, *args, **kwargs):
     ...   def fn_part(*fn_args, **fn_kwargs):
     ...     kwargs.update(fn_kwargs)
     ...     return fn(*args + fn_args, **kwargs)
     ...   return fn_part
     ...
     >>> my_stuff = partial(get_stuff, 'myuser', 'mypwd')
     >>> my_stuff(3)
     get_stuff called with user: myuser, pw: mypwd, stuff_id: 3
     >>> my_stuff(67)
     get_stuff called with user: myuser, pw: mypwd, stuff_id: 67

 Частичные компоненты могут использоваться во многих местах для устранения дублирования кода, когда функция вызывается в разных местах с одинаковыми или почти одинаковыми аргументами. Конечно, вам не нужно реализовывать это самостоятельно; просто сделай из functools частичный импорт.

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

    System Message: ERROR/3 (<string>, line 142)

    Unknown directive type "code".

    .. code:: python
     >>> @print_call
     ... def will_be_logged(arg):
     ...   return arg*5
     ...
     >>> will_be_logged("!")
     Calling will_be_logged with arguments:
          args: ('!',)
          kwargs:{}
     will_be_logged returning '!!!!!'
     '!!!!!'

Using the @-notation is simply a convenient shorthand to doing:

    System Message: ERROR/3 (<string>, line 157)

    Unknown directive type "code".

    .. code:: python
     >>> def will_be_logged(arg):
     ...   return arg*5
     ...
     >>> will_be_logged = print_call(will_be_logged)

 Но что, если мы хотим иметь возможность параметризовать декоратор? В этом случае функция, используемая в качестве декоратора, получит аргументы, и ожидается, что она вернет функцию, которая упаковывает декорированную функцию:

    System Message: ERROR/3 (<string>, line 169)

    Unknown directive type "code".

    .. code:: python
     >>> def require(role):
     ...   def wrapper(fn):
     ...     def new_fn(*args, **kwargs):
     ...       if not role in kwargs.get('roles', []):
     ...         print("%s not in %s" % (role, kwargs.get('roles', [])))
     ...         raise Exception("Unauthorized")
     ...       return fn(*args, **kwargs)
     ...     return new_fn
     ...   return wrapper
     ...
     >>> @require('admin')
     ... def get_users(**kwargs):
     ...   return ('Alice', 'Bob')
     ...
     >>> get_users()
     admin not in []
     Traceback (most recent call last):
       File "<stdin>", line 1, in <module>
       File "<stdin>", line 7, in new_fn
     Exception: Unauthorized
     >>> get_users(roles=['user', 'editor'])
     admin not in ['user', 'editor']
     Traceback (most recent call last):
       File "<stdin>", line 1, in <module>
       File "<stdin>", line 7, in new_fn
     Exception: Unauthorized
     >>> get_users(roles=['user', 'admin'])
     ('Alice', 'Bob')

 … и вот оно у тебя. Теперь вы готовы писать декораторы и, возможно, использовать их для написания аспектно-ориентированного Python; добавление @cache, @trace, @throttle все тривиально (и прежде чем добавлять @cache, проверьте еще раз functools, если вы используете Python 3!).