Pull to refresh

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

Reading time 4 min
Views 17K
Когда я только начинал изучать Python, большое впечатление на меня произвели route-декораторы в известном фреймворке flask. Конечно, я догадывался, как они могли быть реализованы, но как всегда желание писать (а не читать) превзошло необходимость взглянуть на исходный код flask, и мне пришлось выдумать то, что могло бы выглядеть так же лапидарно, как вышеупомянутые декораторы из flask'а. Упражнение на тему замыканий, декораторов и области видимости в Python могло бы выглядеть так:

def do_something(p):
    return p

@implements(do_something, lambda: not p % 2)
def do_mod2_something(p):
    return p / 2

@implements(do_something, lambda: not p % 3)
def do_mod3_something(p):
    return p / 3

do_something(10)  # returns 5
do_something(9)   # returns 3
do_something(11)  # returns 11


Как реализовать декоратор @implements? Может ли подобная реализация использоваться где-то в реальных проектах — вопрос, который мы редко принимаем во внимание, выдумывая себе упражнения для понимания того, как работают те или иные программы. Мне показалось, что это выглядит как некое замещение (override) функции, имеющее место в других языках программирования.

Override


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

#include <iostream>

int sum(int a, int b)
{
    std::cout << "int" << std::endl;
    return a + b;
}

double sum(double a, double b)
{
    std::cout << "double" << std::endl;
    return a + b;
}

int main(void)
{
    std::cout << sum(1, 2) << std::endl;
    std::cout << sum(1.1, 3.0) << std::endl;
    return 0;
}

В языках программирования с динамической типизацией нужды поддерживать реализации для разных типов данных практически нет. Однако, что если у нас появится возможность запускать различные реализации функции в зависимости от значений аргументов? Например, в FSM, где на каждый шаг необходимо проверять текущее состояние и выполнять переход к другому. Или в реализации каких-либо очень платформенно-зависимых функций. Можем ли мы каким-либо образом, без использования цепочек из if-then-else реализовать подобное на Python?

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

Функции


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

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

In [1]: def m(p):
   ...:     def s():
   ...:         return p
   ...:     return s
   ...: 

In [2]: x = m(10)

In [3]: x.func_closure
Out[3]: (<cell at 0x10cd547f8: int object at 0x7f89ab505860>,)

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

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

@implements


def implements(orig_obj, requirements=lambda: False):
    ...

Декоратор объявляет реализацию декорируемого объекта orig_obj в случае, если во время вызова выполняются условия requirements. Пример использования был приведен в начале статьи. Реализация декоратора не позволяет вызывать из реализации функции orig_obj, но это легко решается добавлением дополнительных атрибутов функциям и их проверке во время вызова декорируемой функции.

В двух словах о том, как работает декоратор. При вызове декоратор ищет orig_obj в глобальном пространстве имен с помощью функции globals(). Это необходимо, чтобы заместить вызов оригинальной функции обработчиком orig_wrapper.

Далее проверяется, является ли найденый по имени объект оберткой для оригинальной функции с помощью проверки наличия атрибута __orig_wrapper__. Если этот атрибут отсутствует, то выполняется замещение. Замещающей функции добавляется атрибут __impl__ для хранения реализаций и условий (requirements).

Как только был вызван первый декоратор, do_something изменяет свое поведение таким образом, что прежде, чем выполнить собственную реализацию, проверяет все условия requirements, и если какое-либо условие выполняется, то будет вызвана задекорированная функция. В реализации используется вышеупомянутый атрибут функции func_globals для того, чтобы лямбда-выражение выполнялось в необходимом контексте.

Исходный код @implements
import functools


def implements(orig_obj, requirements=lambda: False):

    def orig_wrapper(*args, **kwargs):
        for impl in orig_obj.__impl_lookup__.__impl__:
            impl[0].func_globals.update(kwargs)
            impl[0].func_globals.update(dict(zip(
                orig_obj.func_code.co_varnames,
                args
            )))

            if impl[0]():
                return impl[1](*args, **kwargs)

        return orig_obj(*args, **kwargs)

    setattr(orig_wrapper, '__orig_wrapper__', True)

    def impl_wrapper(obj):
        orig = globals()[orig_obj.__name__]

        if not hasattr(orig, '__orig_wrapper__'):
            setattr(orig_wrapper, '__impl__', [])
            functools.update_wrapper(
                orig_wrapper,
                globals()[orig_obj.__name__]
            )
            globals()[orig_obj.__name__] = orig_wrapper

            setattr(orig, '__impl_lookup__', orig_wrapper)
            orig = globals()[orig_obj.__name__]

        orig.__impl__.append((requirements, obj))

        # do not change behaviour of the implementation
        return obj

    return impl_wrapper

Заключение


Не уверен, что данный подход к организации различных реализаций может быть удобным и «идеологически» верным, но изучение и работа над этим примером были для меня хорошим упражнением для понимания того, как работают замыкания и области видимости в Python.
Tags:
Hubs:
+15
Comments 1
Comments Comments 1

Articles