Когда я только начинал изучать Python, большое впечатление на меня произвели route-декораторы в известном фреймворке flask. Конечно, я догадывался, как они могли быть реализованы, но как всегда желание писать (а не читать) превзошло необходимость взглянуть на исходный код flask, и мне пришлось выдумать то, что могло бы выглядеть так же лапидарно, как вышеупомянутые декораторы из flask'а. Упражнение на тему замыканий, декораторов и области видимости в Python могло бы выглядеть так:
Как реализовать декоратор @implements? Может ли подобная реализация использоваться где-то в реальных проектах — вопрос, который мы редко принимаем во внимание, выдумывая себе упражнения для понимания того, как работают те или иные программы. Мне показалось, что это выглядит как некое замещение (override) функции, имеющее место в других языках программирования.
В языках со статической типизацией данных имеет место такой прием как замещение реализации функции. С помощью сигнатуры во время компиляции выбирается подходящая для вызова функция. В C++ и Java, например, этот прием часто используется для того, чтобы иметь несколько реализаций функции для аргументов различных типов данных. Чтобы до конца представить, о чем идет речь, ниже приведен почти канонический пример замещения функции на C++:
В языках программирования с динамической типизацией нужды поддерживать реализации для разных типов данных практически нет. Однако, что если у нас появится возможность запускать различные реализации функции в зависимости от значений аргументов? Например, в FSM, где на каждый шаг необходимо проверять текущее состояние и выполнять переход к другому. Или в реализации каких-либо очень платформенно-зависимых функций. Можем ли мы каким-либо образом, без использования цепочек из if-then-else реализовать подобное на Python?
Кажется, что на Python можно реализовать практически все. Конечно, не без возможных потерь в производительности, но наличие таких мощных инструментов как замыкания и декораторы открывает простор для реализаций собственных велосипедов и нездоровых фантазий.
Функции являются объектами первого класса. Об этом написано в каждой книге по программированию на языке Python. Это дает возможность создавать функции во время выполнения, менять их атрибуты и вообще обращаться с ними как с обычными объектами.
О декораторах уже достаточно много написано не только на этом ресурсе, поэтому сильно углубляться в эту тему не хочется. Замыкания представляют собой объекты функций, которые хранят вместе с собой окружение. По сути каждая декорированная функция представляет собой замыкание, неся с собой не только код функции, но и все окружение, которое существовало внутри декоратора во время определения функции:
Из данного примера видно, функция x() содержит в себе информацию о целочисленном объекте. Этот объект будет существовать до тех пор, пока будет существовать функция x().
Помимо этого функция содержит в себе информацию об окружении, в котором она была определена. Для этого используется атрибут func_globals, представленный словарем, который поддается изменениям. Эти особенности будут использованы для реализации декоратора @implements.
Декоратор объявляет реализацию декорируемого объекта orig_obj в случае, если во время вызова выполняются условия requirements. Пример использования был приведен в начале статьи. Реализация декоратора не позволяет вызывать из реализации функции orig_obj, но это легко решается добавлением дополнительных атрибутов функциям и их проверке во время вызова декорируемой функции.
В двух словах о том, как работает декоратор. При вызове декоратор ищет orig_obj в глобальном пространстве имен с помощью функции globals(). Это необходимо, чтобы заместить вызов оригинальной функции обработчиком orig_wrapper.
Далее проверяется, является ли найденый по имени объект оберткой для оригинальной функции с помощью проверки наличия атрибута __orig_wrapper__. Если этот атрибут отсутствует, то выполняется замещение. Замещающей функции добавляется атрибут __impl__ для хранения реализаций и условий (requirements).
Как только был вызван первый декоратор, do_something изменяет свое поведение таким образом, что прежде, чем выполнить собственную реализацию, проверяет все условия requirements, и если какое-либо условие выполняется, то будет вызвана задекорированная функция. В реализации используется вышеупомянутый атрибут функции func_globals для того, чтобы лямбда-выражение выполнялось в необходимом контексте.
Не уверен, что данный подход к организации различных реализаций может быть удобным и «идеологически» верным, но изучение и работа над этим примером были для меня хорошим упражнением для понимания того, как работают замыкания и области видимости в 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.
