В первой части серии публикаций о продвинутых возможностях Python мы познакомились с итераторами, генераторами и модулем itertools. В сегодняшней публикации речь пойдёт о замыканиях, декораторах и модуле functools.
- Декораторы
- Декораторы класса
- Несколько примеров из Flask
- Дополнительное чтение
- Приложение: замыкания
Декораторы
Декоратор — паттерн проектирования, при использовании которого класс или функция изменяет или дополняет функциональность другого класса или функции без использования наследования или прямого изменения исходного кода. В Python декораторы представляют собой функции или любые вызываемые объекты, которые принимают на вход набор необязательных аргументов и функцию или класс и возвращают функцию или класс. Их можно использовать для реализации паттерна проектирования декоратора или для решения других задач. Декораторы классов появились в Python 2.6.
Кстати, если вы не знакомы с замыканиями Python, прежде чем читать дальше ознакомьтесь с дополнением о замыканиях в конце этой статьи. Концепцию декораторов сложно понять, если вы не знакомы с замыканиями.
В Python декораторы применяются к функции или классу с помощью символа @
. В качестве первого примера давайте используем простой декоратор, который регистрирует вызовы функций. В этом примере декоратор принимает формат времени в качестве аргумента и печатает лог перед и после выполнения декорированной функции с временем исполнения. Это может быть кстати, когда вы сравниваете эффективность разных реализаций алгоритма или разных алгоритмов.
def logged(time_format):
def decorator(func):
def decorated_func(*args, **kwargs):
print("- Running '{}' on {} ".format(
func.__name__,
time.strftime(time_format)
))
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print("- Finished '{}', execution time = {:0.3f}s ".format(
func.__name__,
end_time - start_time
))
return result
decorated_func.__name__ = func.__name__
return decorated_func
return decorator
Посмотрите на пример использования. Здесь функции add1
и add2
оформлены с помощью logged
, а также дан пример вывода. Заметьте, что формат времени хранится в замыкании возвращаемых функций с декоратором. Поэтому понимание замыканий необходимо для понимания декораторов Python.
Также обратите внимание, как имя возвращаемой функции заменяется именем оригинальной функции в случае, если оно используется позже. Python не делает этого по умолчанию.
@logged("%b %d %Y - %H:%M:%S")
def add1(x, y):
time.sleep(1)
return x + y
@logged("%b %d %Y - %H:%M:%S")
def add2(x, y):
time.sleep(2)
return x + y
print(add1(1, 2))
print(add2(1, 2))
# Output:
- Running 'add1' on Jul 24 2013 - 13:40:47
- Finished 'add1', execution time = 1.001s
3
- Running 'add2' on Jul 24 2013 - 13:40:48
- Finished 'add2', execution time = 2.001s
3
Если вы достаточно внимательны, то заметите, что мы заботимся, чтобы у возвращаемой функции был правильно указан __name__
, но не заботимся о __doc__
или __module__
. Поэтому если у функции add
есть строка документации, она потеряется. Как можно этого избежать? Мы могли бы справиться с проблемой так же, как при обработке __name__
. Но выполнять такие операции с каждым декоратором утомительно. Поэтому в модуле functools
есть декоратор wraps
, который срабатывает именно в таком сценарии. Использование декоратора внутри другого декоратора может показаться странным. Но если вы думаете о декораторах как о функциях, которые принимают функции в качестве параметров и возвращают функции, всё становится на места. Декоратор wraps
используется в следующих примерах вместо ручной обработки __name__
и других подобных атрибутов.
Следующий пример немного сложнее. Давайте напишем декоратор, который кэширует результат вызова функции в течение указанного в секундах времени. Код ожидает, что переданные в функцию аргументы — хэшируемые объекты (hashable objects), потому что мы используем кортеж с аргументами args
в качестве первого параметра и замороженный набор элементов в kwargs
в качестве второго параметра, который выступает ключом кэша. У каждой функции будет уникальный кэш dict
, который хранится в замыкании функции.
import time
from functools import wraps
def cached(timeout, logged=False):
"""Decorator to cache the result of a function call.
Cache expires after timeout seconds.
"""
def decorator(func):
if logged:
print("-- Initializing cache for", func.__name__)
cache = {}
@wraps(func)
def decorated_function(*args, **kwargs):
if logged:
print("-- Called function", func.__name__)
key = args, frozenset(kwargs.items())
result = None
if key in cache:
if logged:
print("-- Cache hit for", func.__name__, key)
cache_hit, expiry = cache[key]
if time.time() - expiry < timeout:
result = cache_hit
elif logged:
print("-- Cache expired for", func.__name__, key)
elif logged:
print("-- Cache miss for", func.__name__, key)
# No cache hit, or expired
if result is None:
result = func(*args, **kwargs)
cache[key] = result, time.time()
return result
return decorated_function
return decorator
Вот как это используется. Мы применяем декоратор к наивному и неэффективному калькулятору чисел Фибоначчи. Декоратор кэша эффективно применяет к коду паттерн мемоизации. Обратите внимание, что в замыкании fib
находятся кэш dict
, ссылка на исходную функцию fib
, значение аргумента logged
, а также значение аргумента timeout
. dump_closure
описывается в конце статьи после раздела о замыканиях.
>>> @cached(10, True)
... def fib(n):
... """Returns the n'th Fibonacci number."""
... if n == 0 or n == 1:
... return 1
... return fib(n - 1) + fib(n - 2)
...
-- Initializing cache for fib
>>> dump_closure(fib)
1. Dumping function closure for fib:
-- cell 0 = {}
-- cell 1 = <function fib at 0x10eae7500>
-- cell 2 = True
-- cell 3 = 10
>>>
>>> print("Testing - F(4) = {}".format(fib(4)))
-- Called function fib
-- Cache miss for fib ((4,), frozenset([]))
-- Called function fib
-- Cache miss for fib ((3,), frozenset([]))
-- Called function fib
-- Cache miss for fib ((2,), frozenset([]))
-- Called function fib
-- Cache miss for fib ((1,), frozenset([]))
-- Called function fib
-- Cache miss for fib ((0,), frozenset([]))
-- Called function fib
-- Cache hit for fib ((1,), frozenset([]))
-- Called function fib
-- Cache hit for fib ((2,), frozenset([]))
Testing - F(4) = 5
Декораторы класса
В предыдущем разделе мы рассмотрели декораторы функций и некоторые необычные способы их применения. Теперь давайте рассмотрим декораторы классов. В данном случае декоратор принимает на вход класс (объект с типом type
в Python) и возвращает модифицированный класс.
Первый пример — простая математика. Дано частично упорядоченное множество P. Мы определяем Pd как дуальность P, исключительно если P(x,y)⟺Pd(y,x). Другими словами, речь идёт об обратном порядке. Как можно реализовать это с помощью Python? Предположим, класс определяет порядок с помощью методов __lt__
, __le__
и так далее. Тогда мы можем написать декоратор класса, который заменяет каждую функцию её дуальностью.
def make_dual(relation):
@wraps(relation, ['__name__', '__doc__'])
def dual(x, y):
return relation(y, x)
return dual
def dual_ordering(cls):
"""Class decorator that reverses all the orderings"""
for func in ['__lt__', '__gt__', '__ge__', '__le__']:
if hasattr(cls, func):
setattr(cls, func, make_dual(getattr(cls, func)))
return cls
Вот как это можно применить к str
, чтобы создать новый класс rstr
, в котором используется обратный лексикографический порядок.
@dual_ordering
class rstr(str):
pass
x = rstr("1")
y = rstr("2")
print x < y
print x <= y
print x > y
print x >= y
# Output:
False
False
True
True
Давайте посмотрим на более сложный пример. Предположим, мы хотим применить декоратор logged
из предыдущего примера ко всем методам в классе. Это можно сделать вручную: просто добавить декоратор в каждый метод. Также можно автоматизировать процесс с помощью декоратора класса. Прежде чем сделать это, автор улучшил декоратор logged
из предыдущего раздела. Теперь в нём используется атрибут wraps
из модуля functools
вместо ручной работы с __name__
. Также здесь в возвращаемую функцию добавлен атрибут _logged_decorator
. Его значение True
, он применяется, чтобы избежать двойного декорирования функции. Это удобно, когда мы применяем декоратор к классам, которые должны наследовать методы от других классов. Наконец, добавлен аргумент name_prefix
, который делает возможной кастомизацию сообщений лога.
def logged(time_format, name_prefix=""):
def decorator(func):
if hasattr(func, '_logged_decorator') and func._logged_decorator:
return func
@wraps(func)
def decorated_func(*args, **kwargs):
start_time = time.time()
print("- Running '{}' on {} ".format(
name_prefix + func.__name__,
time.strftime(time_format)
))
result = func(*args, **kwargs)
end_time = time.time()
print("- Finished '{}', execution time = {:0.3f}s ".format(
name_prefix + func.__name__,
end_time - start_time
))
return result
decorated_func._logged_decorator = True
return decorated_func
return decorator
Теперь можно написать декоратор класса.
def log_method_calls(time_format):
def decorator(cls):
for o in dir(cls):
if o.startswith('__'):
continue
a = getattr(cls, o)
if hasattr(a, '__call__'):
decorated_a = logged(time_format, cls.__name__ + ".")(a)
setattr(cls, o, decorated_a)
return cls
return decorator
Вот как он будет использоваться. Обратите внимание, как здесь обрабатываются переопределённые методы и наследование.
@log_method_calls("%b %d %Y - %H:%M:%S")
class A(object):
def test1(self):
print("test1")
@log_method_calls("%b %d %Y - %H:%M:%S")
class B(A):
def test1(self):
super().test1()
print("child test1")
def test2(self):
print("test2")
b = B()
b.test1()
b.test2()
# Output:
- Running 'B.test1' on Jul 24 2013 - 14:15:03
- Running 'A.test1' on Jul 24 2013 - 14:15:03
test1
- Finished 'A.test1', execution time = 0.000s
child test1
- Finished 'B.test1', execution time = 1.001s
- Running 'B.test2' on Jul 24 2013 - 14:15:04
test2
- Finished 'B.test2', execution time = 2.001s
Наш первый пример декораторов класса должен был изменять порядок методов класса. Похожий декоратор, но более полезный, может принимать один из __lt__
, __le__
, __gt__
или __ge__
и __eq__
, и реализовывать остальные для полного упорядочивания класса. Это именно то, что делает декоратор functools.total_ordering
. Подробности в документации.
Несколько примеров из Flask
Рассмотрим несколько интересных примеров использования декораторов в Flask.
Представьте, что хотите, чтобы некоторые функции выводили предупреждающие сообщения, если они вызываются при определённых обстоятельствах в режиме отладки. Вместо того, чтобы вручную добавлять код в начало каждой функции, можно использовать декоратор. Это то, что делает декоратор, который можно найти в файле app.py
Flask.
def setupmethod(f):
"""Wraps a method so that it performs a check in debug mode if the
first request was already handled.
"""
def wrapper_func(self, *args, **kwargs):
if self.debug and self._got_first_request:
raise AssertionError('A setup function was called after the '
'first request was handled. This usually indicates a bug '
'in the application where a module was not imported '
'and decorators or other functionality was called too late.\n'
'To fix this make sure to import all your view modules, '
'database models and everything related at a central place '
'before the application starts serving requests.')
return f(self, *args, **kwargs)
return update_wrapper(wrapper_func, f)
Более интересный пример — декоратор Flask route
, который определяется в классе Flask
. Заметьте, что декоратор может быть методом класса. В этом случае в качестве первого параметра используется self
. Полный код смотрите в файле app.py. Обратите внимание, декоратор просто регистрирует декорированную функцию как обработчик URL с помощью вызова функции add_url_rule
.
def route(self, rule, **options):
"""A decorator that is used to register a view function for a
given URL rule. This does the same thing as :meth:`add_url_rule`
but is intended for decorator usage::
@app.route('/')
def index():
return 'Hello World'
For more information refer to :ref:`url-route-registrations`.
:param rule: the URL rule as string
:param endpoint: the endpoint for the registered URL rule. Flask
itself assumes the name of the view function as
endpoint
:param options: the options to be forwarded to the underlying
:class:`~werkzeug.routing.Rule` object. A change
to Werkzeug is handling of method options. methods
is a list of methods this rule should be limited
to (`GET`, `POST` etc.). By default a rule
just listens for `GET` (and implicitly `HEAD`).
Starting with Flask 0.6, `OPTIONS` is implicitly
added and handled by the standard request handling.
"""
def decorator(f):
endpoint = options.pop('endpoint', None)
self.add_url_rule(rule, endpoint, f, **options)
return f
return decorator
Дополнительное чтение
Много информации о декораторах вы найдёте на официальной вики-странице Python. Также можно посмотреть замечательное видео Дэвида Безли о метапрограммировании в Python 3.
Приложение: замыкания
Замыкание — это комбинация функции и множества ссылок на переменные в области видимости функции. Последнее иногда называют ссылочной средой. Замыкание позволяет выполнять функцию за пределами области видимости. В Python ссылочная среда хранится в виде набора ячеек. Доступ к ним можно получить с помощью атрибутов func_closure
или __closure__
. В Python 3 используется только __closure__
.
Важно понимать, что речь идёт просто о ссылках, а не о глубоких копиях объектов. Конечно, неважно, являются ли объекты неизменяемыми, но для изменяемых объектов, например, списков, это важно. Это иллюстрирует пример ниже. Обратите внимание, у функций также есть __globals__
, где хранится глобальное ссылочное окружение, для которого была определена функция. Посмотрите на простой пример:
>>> def return_func_that_prints_s(s):
... def f():
... print(s)
... return f
...
>>> g = return_func_that_prints_s("Hello")
>>> h = return_func_that_prints_s("World")
>>> g()
Hello
>>> h()
World
>>> g is h
False
>>> h.__closure__
(<cell at 0x10d172398: str object at 0x10d170840>,)
>>> print([str(c.cell_contents) for c in g.__closure__])
['Hello']
>>> print([str(c.cell_contents) for c in h.__closure__])
['World']
Ещё один пример, более сложный. Убедитесь, что понимаете, почему код работает именно так.
>>> def return_func_that_prints_list(z):
... def f():
... print(z)
... return f
...
>>> z = [1, 2]
>>> g = return_func_that_prints_list(z)
>>> g()
[1, 2]
>>> z.append(3)
>>> g()
[1, 2, 3]
>>> z = [1]
>>> g()
[1, 2, 3]
Наконец, вот пример метода dump_closure
, который использовался выше.
def dump_closure(f):
if hasattr(f, "__closure__") and f.__closure__ is not None:
print("- Dumping function closure for {}:".format(f.__name__))
for i, c in enumerate(f.__closure__):
print("-- cell {} = {}".format(i, c.cell_contents))
else:
print(" - {} has no closure!".format(f.__name__))
Адаптированный перевод статьи A Study of Python's More Advanced Features Part II: Closures, Decorators and functools by Sahand Saba. Мнение автора оригинальной публикации может не совпадать с мнением администрации «Хекслета».