среда, 9 мая 2012 г.

Выражение exec и Тайна Python'a (перевод)

Какое-то время назад я исследовал объекты кода с использованием модуля dis. В том посте я привёл несколько примеров выполнения объектов кода, созданных налету с использованием выражения exec. Теперь мы продолжим наши исследования.
Учтите, что как и мой предыдущий пост, эти исследования касаются только Python 2.x, в частности Python 2.7. В отличие от того поста, как минимум один способ работать не будет в Python 3.x - в 3.х exec является функцией и изменения в локальной области видимости не переносятся в область видимости вызывающей функции (спасибо comex, который отметил это)

Синопсис. Или что было в начале

Перед тем, как мы нырнём в exec, вспомним, что compile() преобразует строку кода на Python в объект кода так же, как это делается при выполнении любого скрипта на Python. Эта функция принимает три аргумента: строку с кодом для компиляции, имя файла с исходным кодом (для которого обычно используется "" если код взят не из файла) и способ компиляции (чаще всего это будет "exec", по крайней мере в наших экспериментах)

Поехали!


Нельзя сказать, что сам по себе объект кода безумно полезен. Конечно, в нём содержится информация, необходимая для выполнения кода, но это всего лишь существительное, которое само по себе не может быть исполнено:
>>> code_object = compile("""
... print "Hello, world"
... """, '<string>', 'exec')
>>> code_object()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'code' object is not callable
Не очень-то полезно, да? Кончено, мы можем использовать модуль dis для исследования байт-кода и его атрибутов, чтобы вычислить используемые переменные, найти исходный код для этого объекта и т.д., но что если мы просто хотим его запустить? Используем выражение exec:
>>> exec code_object
Hello, world
Это больше похоже на правду!

Игры с exec

Представим, что у нас есть функция, которая принимает объект кода и аргумент х:
>>> def exec_code_and_return_x(code_object, x):
... exec code_object
... return x
...
Может быть Вы ждёте, что получите значение х без изменений? В конце-концов код функции никак не взаимодействует с х. Однако, в зависимости от объекта кода, Вы можете получить не совсем то, что хотите:
>>> my_code_object = compile("""
... x = x + 1
... """, '<string>', 'exec')
>>> exec_code_and_return_x(my_code_object, 1):
2
Но при этом Вы не всегда получите ожидаемый неожиданный результат:
>>> my_code_object = compile("""
... del x
... """, '<string>', 'exec')
>>> exec_code_and_return_x(my_code_object, 1)
1
Так какого чёрта тут происходит?

exec и области тьмы видимости

Без дополнительных указаний exec использует текущие глобальные и локальные пространства имён для выполнения кода. Сие значит (как мы уже видели), что объект кода, который выполняется может изменять переменные в области видимости (так же как и другие изменяемые объекты).
Но Вы можете изменить области видимости (если Вам это понадобится) выполнения используя ключевое слово in:
>>> code_globals = {}
>>> code_locals = {}
>>> code_object = compile("""
... x = 1
... """, '<string>', 'exec')
>>> exec code_object in code_globals, code_locals
>>> print code_locals
{'x': 1}
Обратите внимание, что встроенное пространство имён (builtin), как и следует из названия, доступно объекту кода всегда:



>>> code_globals = {}
>>> code_locals = {}
>>> code_object = compile("""
... print unicode("Hello, world")
... """, '<string>', 'exec')
>>> exec code_object in code_globals, code_locals
Hello, world
>>> code_globals.keys()
['__builtins__']

Разоблачение тайны

Возвращаясь к нашей загадке:
>>> my_code_object = compile("""
... del x
... """, '<string>', 'exec')
>>> exec_code_and_return_x(my_code_object, 1)
1
Почему же мы не можем удалить нашу локальную переменную? Более того, чем наш код отличается от этого:
>>> def delete_local_then_return_it():
...     x = 1
...     del x
...     return x
...
>>> delete_local_then_return_it()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in delete_local_then_return_it
UnboundLocalError: local variable 'x' referenced before assignment
То, свидетелями чего мы оказались, является результатом оптимизации Python'a: вместо того, чтобы для хранения локальных переменных использовать словарь (или другой отображаемый объект (mapping object)), в нашем случае Python использует кортеж, к которому обращается по номеру позиции для получения переменной. Как следствие такой оптимизации динамический код (такой, как возвращаемый compile() или при применении exec напрямую к строке) не может изменить размер массива с локальными переменными, так как он фиксируется во время компиляции.
Более того, реализация exec такова, что для выполнения создаётся новый стек Python, то есть у Вас есть свой собственный массив локальных переменных (используемых кодом, который выполняется):
>>> import inspect
>>> def get_stack_depth():
...     frame = inspect.currentframe()
...     # begin depth at -1 to account for the extra
...     # frame created when calling get_stack-depth
...     depth = -1
...     while frame:
...             depth += 1
...             frame = frame.f_back
...     return depth
... 
>>> def show_stack_depths():
...     print 'stack depth in function', get_stack_depth()
...     exec compile("""
...     print "stack depth in exec", get_stack_depth()
...     """, '<string>', 'exec')
... 
>>> show_stack_depths()
stack depth in function 2
stack depth in exec 3
Объекты кода могут, конечно, изменять локальные переменные в своей области видимости (в своём стеке):
>>> def show_code_object_locals():
...     code_globals = {}
...     code_locals = {'x': 1}
...     exec compile("""del x""", '<string>', 'exec') in code_globals, code_locals
...     return code_locals
... 
>>> show_code_object_locals()
{}

Предупреждения и подводные камни

Использовать exec для динамического выполнения кода конечно прикольно, но не безопасно.
Во первых, хорош бы я был, если бы я не сказал Вам: никогда не используйте exec; Python не предоставляет Вам никакого способа (да и вообще нет такого способа), который бы гарантировал Вам, что код, который Вы хотите выполнить не навредит вашей системе. Воспринимайте это как разновидность cross-site scripting attacks. Если Вы хотите испольнять код, предоставленный пользователем на вашей системе - Вы что-то делаете не так.
Во вторых, такое выполнение кода заметно медленнее, чем просто выполнение кода, так как обычный код оптимизируется Python'ом, а динамический - нет.
И наконец, если Вам надо использовать exec, Вы можете решить, что гораздо проще передать строку или объект файла в качестве первого аргумента, но надо понимать, что это повлечёт накладные расходы на компиляцию каждый раз при вызове exec. Если Вам надо выполнять один и тот же кусок кода несколько раз, то гораздо быстрее будет один раз его скомпилировать при помощи compile() и затем повторно вызывать.

2 комментария: