На этом шаге мы рассмотрим особенности использования exec().
Вы используете exec() для выполнения фрагмента кода в области видимости вызывающего (caller), но после выполнения никаких результатов не видно.
Чтобы лучше понять проблему, проведите небольшой эксперимент. Для начала выполним фрагмент кода в глобальном пространстве имен:
>>> a = 13 >>> exec('b = a + 1') >>> print(b) 14 >>>
>>> def test(): a = 13 exec('b = a + 1') print(b) >>> test() Traceback (most recent call last): File "<stdin>", line 1, In <module> File "<stdin>", line 4, in test NameError: global name 'b' is not defined >>>
Как вы можете видеть, программа падает с ошибкой NameError() - практически так же, как если бы инструкция exec() вообще не была выполнена. Это может стать проблемой, если вы захотите использовать результат exec() в последующих вычислениях.
Чтобы исправить проблемы такого типа, вам нужна функция locals(). Она позволяет получить словарь локальных переменных перед вызовом exec(). Сразу после этого вы можете извлечь измененные значения из данного словаря. Например:
>>> def test(): a = 13 loc = locals() exec('b = a + 1') b = loc['b'] print(b) >>> test() 14 >>>
Правильное использование exec() на практике - довольно сложная штука. На самом деле в большинстве случаев, когда вы могли бы задуматься о применении exec(), можно поискать более элегантное решение (например, декораторы, замыкания, метаклассы и т. д.).
Однако если вы должны использовать именно exec(), этот рецепт подчеркивает некоторые тонкие аспекты того, как делать это правильно. По умолчанию exec() выполняет код в локальной и глобальной областях видимости вызывающего. Однако внутри функций локальная область видимости передается в exec() как словарь, который является копией настоящих локальных переменных. Так что если код, который выполняется в exec(), вносит какие-либо изменения, эти изменения никогда не отражаются на настоящих локальных переменных. Вот еще один пример, который демонстрирует этот эффект:
>>> def test1(): x = 0 exec('x += 1') print(x) >>> test1() 0 >>>
Когда вы вызываете locals(), чтобы получить локальные переменные, как показано в решении, вы получаете копию, которая передана в exec(). Путем инспектирования значений словаря после выполнения вы можете получить измененные значения. Вот эксперимент, который это демонстрирует:
>>> def test2(): x = 0 loc = locals() print('before:', loc) exec('x += 1') print('after:', loc) print('x =', x) >>> test2() before: {'x': 0} after: {'x': 1, 'loc': {...}} x = 0 >>>
Обратите внимание на вывод на последнем шаге. До тех пор, пока вы не скопировали значение из loc обратно в x, переменная оставалась неизменной.
При использовании locals() вам нужно следить за порядком выполнения операций. Каждый раз, когда locals() вызывается, она берет текущие значения локальных переменных и переписывает соответствующие записи в словаре. Понаблюдайте за этим экспериментом:
>>> def test3(): x = 0 loc = locals() print(loc) exec('x += 1') print(loc) locals() print(loc) >>> test3() {'x': 0} {'x': 1, 'loc': {...}} {'x': 0, 'loc': {...}} >>>
Обратите внимание, как последний вызов locals() вызывает перезаписывание х. В качестве альтернативы использованию locals() вы можете создать собственный словарь и передать его в exec(). Например:
>>> def test4(): a = 13 loc = {'a': a} glb = {} exec('b = a + 1', glb, loc) b = loc['b'] print(b) >>> test4() 14 >>>
Для большинства случаев использования exec() это, вероятно, будет хорошей практикой. Вам просто нужно убедиться, что глобальный и локальный словари правильно инициализированы именами, к которым будет обращаться выполняемый код.
И последнее: перед использованием exec() стоит подумать об альтернативах. Многие подобные задачи успешно решаются с помощью замыканий, декораторов, метаклассов или других приемов метапрограммирования.
На следующем шаге мы рассмотрим парсинг и анализ исходного кода Python.