Скидки до 20% + 2-ая профессия бесплатно и подарки на 50 000₽

Главная | Все статьи | Код

Изучаем продвинутые возможности Python, часть 2: замыкания, декораторы, модуль functools

Время чтения статьи ~14 минут 12
Изучаем продвинутые возможности Python, часть 2: замыкания, декораторы, модул... главное изображение

В первой части серии публикаций о продвинутых возможностях Python мы познакомились с итераторами, генераторами и модулем itertools. В сегодняшней публикации речь пойдёт о замыканиях, декораторах и модуле functools.

Декораторы

Декоратор — паттерн проектирования, при использовании которого класс или функция изменяет или дополняет функциональность другого класса или функции без использования наследования или прямого изменения исходного кода. В 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. Мнение автора оригинальной публикации может не совпадать с мнением администрации «Хекслета».

Аватар пользователя Дмитрий Дементий
Дмитрий Дементий 17 октября 2019
12
Похожие статьи
Рекомендуемые программы
профессия
Верстка на HTML5 и CSS3, Программирование на JavaScript в браузере, разработка клиентских приложений используя React
10 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на Python, Разработка веб-приложений и сервисов используя Django, проектирование и реализация REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
Тестирование веб-приложений, чек-листы и тест-кейсы, этапы тестирования, DevTools, Postman, SQL, Git, HTTP/HTTPS, API
4 месяца
с нуля
Старт 26 декабря
профессия
Программирование на Java, Разработка веб-приложений и микросервисов используя Spring Boot, проектирование REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
новый
Google таблицы, SQL, Python, Superset, Tableau, Pandas, визуализация данных, Anaconda, Jupyter Notebook, A/B-тесты, ROI
9 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на PHP, Разработка веб-приложений и сервисов используя Laravel, проектирование и реализация REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на Ruby, Разработка веб-приложений и сервисов используя Rails, проектирование и реализация REST API
5 месяцев
c опытом
Старт 26 декабря
профессия
Программирование на JavaScript в браузере и на сервере (Node.js), разработка бекендов на Fastify и фронтенда на React
16 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на JavaScript, разработка веб-приложений, bff и сервисов используя Fastify, проектирование REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
новый
Git, JavaScript, Playwright, бэкенд-тесты, юнит-тесты, API-тесты, UI-тесты, Github Actions, HTTP/HTTPS, API, Docker, SQL
8 месяцев
c опытом
Старт 26 декабря