Когда я только начинал изучать 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.