[Из песочницы] Псевдопрактический пример замыканий и декораторов
Когда я только начинал изучать 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
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]: (
@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.