Динамическое выполнение выражений в Python: eval()
О встроенной функции eval(), вопросах ее безопасного и эффективного использования в Python. В конце статьи пишем программу, обобщающую полученные знания.
Функция eval()
полезна, когда необходимо выполнить динамически обновляемое выражение Python из какого-либо ввода (например, функции input()
), представленного в виде строки или объекта байт-кода. Это невероятно полезный инструмент, но то, что она может выполнять программный код, имеет важные последствия для безопасности, которые следует учесть перед ее применением.
Статья является сокращенным переводом публикации Леоданиса Посо Рамоса Python eval(): Evaluate Expressions Dynamically. Из этого руководства вы узнаете:
- Как работает
eval()
. - Как использовать
eval()
для динамического выполнения кода. - Как минимизировать риски для безопасности, связанные с использованием
eval()
.
Разбираемся в том, как работает eval()
Вы можете использовать встроеннyю функцию eval()
для динамического исполнения выражений из ввода на основе строки или скомпилированного кода. Если вы передаете в eval()
строку, то функция анализирует ее, компилирует в байт-код и выполняет как выражение Python.
Сигнатура eval()
определена следующим образом:
Первый аргумент expression
содержит выражение, которое необходимо выполнить. Функция также принимает два необязательных аргумента globals
и locals
, о которых мы поговорим в соответствующих разделах. Начнём по порядку – с аргумента expression
.
exec()
. Основное различие между eval()
и exec()
состоит в том, что eval()
может выполнять лишь выражения, тогда как функции exec()
можно «скормить» любой фрагмент кода Python.Первый аргумент: expression
Когда мы вызываем eval()
, содержание expression
воспринимается интерпретатором как выражение Python. Посмотрите на следующие примеры, принимающие строковый ввод:
При вызове eval()
со строковым выражением в качестве аргумента, функция возвращает значение, полученное в результате оценки входной строки. По умолчанию eval()
имеет доступ к глобальным именам, таким как x
в приведенном выше примере.
Чтобы оценить строковое выражение, eval()
выполняет следующую последовательность действий:
- Парсинг выражения.
- Компилирование в байт-код.
- Выполнение кода выражения Python.
- Возвращение результата.
Имя аргумента expression
подчеркивает, что функция работает только с выражениями, но не составными конструкциями. При попытке передачи блока кода вместо выражения будет получено исключение SyntaxError
:
Таким образом, в eval()
нельзя передать конструкции c if
, import
, def
или class
, с циклами for
и while
. Однако ключевое слово for
может использоваться в eval()
в случае выражений для генераторов списков.
В eval()
запрещены и операции присваивания:
SyntaxError
также вызывается в случаях, когда eval()
не удается распарсить выражение из-за ошибки в записи:
В eval()
можно передавать объекты кода (code objects). Чтобы скомпилировать код, который вы собираетесь передать eval()
, можно использовать compile()
. Это встроенная функция, которая может компилировать строку в объект кода или AST-объект.
Детали того, как использовать compile()
, выходят за рамки этого руководства, но здесь мы кратко рассмотрим первые три обязательных аргумента:
source
содержит исходный код, который необходимо скомпилировать. Этот аргумент принимает обычные строки, байтовые строки и объекты AST.filename
определяет файл, из которого прочитан код. Если используется строчный ввод, значение аргумента должно быть равно строке<string>
.mode
указывает, какой тип объекта кода мы хотим получить. Если нужно обработать код с помощьюeval()
, в качестве значения аргумента указывается"eval"
compile()
. Таким образом, мы можем использовать compile()
для предоставления объектов кода вeval()
вместо обычных строк:
Использование объектов кода полезно при многократном вызове. Если мы предварительно скомпилируем входное выражение, то последующие вызовы eval()
будут выполняться быстрее, так как не будут повторяться шаги синтаксического анализа и компиляции.
Второй аргумент: globals
Аргумент globals
опционален. Он содержит словарь, обеспечивающий доступ eval()
к глобальному пространству имен. С помощью глобальных переменных можно указать eval()
, какие глобальные имена использовать при выполнении выражения.
Глобальные имена – это все те имена, которые доступны в текущей глобальной области или пространстве имен. Вы можете получить к ним доступ из любого места в вашем коде.
Все имена, переданные глобальным переменным в словаре, будут доступны eval()
во время выполнения.
Любые глобальные имена, определенные вне пользовательского словаря globals
, не будут доступны изнутри eval()
, будет вызвано исключение NameError
.
Вы также можете указать имена, которых нет в текущей глобальной области видимости. Чтобы это работало, нужно указать конкретное значение для каждого имени. Тогда eval()
будет интерпретировать эти имена, как если бы это были глобальные переменные:
Если вы предоставите eval()
пользовательский словарь, который не содержит значения для ключа "__builtins__"
, то ссылка на словарь встроенных функций всё равно будет автоматически добавлена к ключу "__builtins__"
, прежде чем выражение будет проанализировано. Это гарантирует, что eval()
имеет полный доступ ко всем встроенным именам Python при оценке выражения.
Несмотря на переданный пустой словарь ({}
), eval()
имеет доступ к встроенным функциям.
При вызове eval()
без передачи пользовательского словаря в глобальные переменные аргумент по умолчанию будет использовать словарь, возвращаемый globals()
в среде, где вызывается eval()
:
Таким образом, передача словаря в аргументе globals
служит как способ намеренно ограничить область видимость имен для функции eval()
.
Третий аргумент: locals
Аргумент locals
также является необязательным аргументом. В этом случае словарь содержит переменные, которые eval()
использует в качестве локальных имен при оценке выражения.
Локальными называются те имена (переменные, функции, классы и т.д.), которые мы определяем внутри данной функции. Локальные имена видны только изнутри включающей функции.
Обратите внимание, что для передачи словаря locals
сначала необходимо предоставить словарь для globals
. Передача по ключу в случае eval()
не работает:
Главное практическое различие между globals
и locals
заключается в том, что Python автоматически вставит ключ "__builtins__"
в globals
, если этот ключ еще не существует. Cловарь locals
остается неизменным во время выполнения eval()
.
Выполнение выражений с eval()
Функция eval()
используется, когда нужно динамически изменять выражения, а применение других техник и инструментов Python требует избыточных усилий. В этом разделе мы обсудим, как использовать eval()
для булевых, математических и прочих выражений Python.
Булевы выражения
Булевы выражения – это выражения Python, которые возвращают логическое значение. Обычно они используются для проверки, является ли какое-либо условие истинным или ложным:
Зачем же может потребоваться использовать eval()
вместо непосредственного применения логического выражения? Предположим, нам нужно реализовать условный оператор, но вы хотите на лету менять условие:
Внутри func()
для оценки предоставленного условия используется функция eval()
, возвращающая a+b
или a-b
в соответствии с результатом оценки.
Теперь представьте, как бы вы реализовали то же поведение без eval()
для обработки любого логического выражения.
Математические выражения
Один из распространенных вариантов использования eval()
в Python – оценка математических выражений из строкового ввода. Например, если вы хотите создать калькулятор на Python, вы можете использовать eval()
, чтобы оценить вводимые пользователем данные и вернуть результат вычислений:
Выражения общего вида
Вы можете использовать eval()
и с более сложными выражениями Python, включающими вызовы функций, создание объектов, доступ к атрибутам и т. д.
Например, можно вызвать встроенную функцию или функцию, импортированную с помощью стандартного или стороннего модуля. В следующих примерах eval()
используется для запуска различных системных команд.
Таким образом, можно передавать команды через какой-либо строковый интерфейс (например, форму в браузере) и выполнять код Python.
Однако по той же причине eval()
может подвергнуть нас серьезным угрозам безопасности, например, позволит злоумышленнику запускать системные команды или выполнять кода на нашем компьютере. В следующем разделе мы обсудим способы устранения угроз безопасности, связанных с eval()
.
Минимизация проблем безопасности, связанных с eval()
В связи с проблемами безопасности обычно рекомендуется по возможности не использовать eval()
. Но если вы решили, что функция необходима, простое практическое правило состоит в том, чтобы никогда не использовать ее для любого ввода, которому нельзя доверять. Сложность в том, чтобы выяснить, каким видам ввода доверять можно .
В качестве примера того, как безответственное использование eval()
может сделать ваш код небезопасным, предположим, что вы хотите создать онлайн-сервис для оценки произвольных выражений Python. Ваш пользователь вводит выражения и нажимает кнопку «Выполнить». Приложение получает пользовательский ввод и передает его для выполнения в eval()
.
Если вы используете Linux и приложения имеет необходимые разрешения, то злонамеренный пользователь может ввести опасную строку, подобную следующей:
Выполнение выражения удалит все файлы в текущей директории.
__import__()
– это встроенная функция, которая принимает имя модуля в виде строки и возвращает ссылку на объект модуля. __import__()
– это функция, которая полностью отличается от оператора import
. Как мы упоминали выше, вы не можете вызвать оператор импорта с помощью eval()
.Ограничение globals и locals
Вы можете ограничить среду выполнения eval()
, задавая собственные словари аргументам globals
и locals
. Например, пустые словари для обоих аргументов, чтобы eval()
не мог получить доступ к именам в текущей области или пространстве имен вызывающей стороны:
К сожалению, это ограничение не устраняет другие проблемы безопасности, связанные с использованием eval()
, поскольку остается доступ ко всем встроенным именам и функциям Python.
Ограничение __builtins__
Как мы видели ранее, перед синтаксическим анализом выражения eval()
автоматически вставляет ссылку на словарь __builtins__
в globals
. Злоумышленник может использовать это поведение, используя встроенную функцию __import__()
, чтобы получить доступ к стандартной библиотеке или любому стороннему модулю, установленному в системе:
Чтобы минимизировать риски, можно переопределить __builtins__
в globals
:
Ограничение имён во входных данных
Однако даже после таких ухищрений Python останется уязвим. Например, можно получить доступ к объекту класса, используя литерал типа, например ""
, []
, {}
или ()
, а также некоторые специальные атрибуты:
Получив доступ к объекту, можно использовать специальный метод .__subclasses__()
, чтобы получить доступ ко всем классам, которые наследованы объектом. Вот как это работает:
Этот код напечатает большой список классов. Некоторые из этих классов довольно мощные и могут быть чрезвычайно опасны в чужих руках. Это открывает еще одну важную дыру в безопасности, которую вы не сможете закрыть, просто ограничивая окружение eval()
:
Генератор списка в приведенном коде фильтрует классы объекта, чтобы вернуть список, содержащий класс range
. Далее range
вызывается для создания соответствующего объекта. Это хитрый способ обойти исключение TypeError
, вызываемое в результате ограничения "__builtins__"
.
Возможное решение этой уязвимости состоит в том, чтобы ограничить использование имен во входных данных набором безопасных имен либо исключить всякое использование любых имен.
Чтобы реализовать эту технику, необходимо выполнить следующие шаги:
- Создать словарь, содержащий имена, которые могут использоваться в
eval()
. - Скомпилировать входную строку в байт-код, используя
compile()
в режимеeval
. - Проверить
.co_names
в объекте байт-кода, чтобы убедиться, что он содержит только разрешенные имена. - Вызвать исключение
NameError
, если пользователь пытается использовать недопустимое имя.
Взглянем на следующую функцию, в которой реализованы все эти шаги:
Эта функция ограничивает имена, которые можно использовать в eval()
, именами в словаре allowed_names
. Для этого функция использует .co_names
– атрибут объекта кода, содержащий кортеж имен в объекте кода.
Следующие примеры показывают, как написанная нами функция eval_expression()
работает на практике:
Если нужно полностью запретить применение имен, достаточно переписать eval_expression()
следующим образом:
Ограничение входных данных до литералов
Типичный пример использования eval()
в Python – это выполнение выражений, содержащих стандартные литералы Python. Задача настолько распространенная, что стандартная библиотека предоставляет соответствующую функцию literal_eval()
. Функция не поддерживает операторы, но работает со списками, кортежами, числами, строками и т. д.:
Использование eval() совместно с input()
В Python 3.x встроенная функция input()
читает пользовательский ввод из командной строки, преобразует его в строку, удаляет завершающий символ новой строки и возвращает результат вызывающей стороне. Поскольку результатом input()
является строка, ее можно передать в eval()
и выполнить как выражение Python:
Это распространенный вариант использования eval()
. Он также эмулирует поведеие input()
в версиях Python 2.x, где функции можно было передать строковое выражение для выполнения (впоследствии от этого отказались из соображений безопасности).
Построим обработчик математических выражений
Итак, мы узнали, как работает eval()
в Python и как использовать функцию на практике. Мы также выяснили, что eval()
имеет важные последствия для безопасности и что обычно считается хорошей практикой избегать использования eval()
в коде. Однако в некоторых ситуациях eval()
может сэкономить много времени и усилий.
В этом разделе вы напишем приложение для оценки математических выражений на лету. Без использования eval()
, нам потребовалось бы выполнить следующие шаги:
- Распарсить входное выражение.
- Преобразовать компоненты выражения в объекты Python (числа, операторы, функции).
- Объединить всё в исполняемое выражение.
- Проверить валидность выражения для Python.
- Выполнить итоговое выражение и вернуть результат вычислений.
Это потребовало бы большой работы, учитывая разнообразие возможных выражений, которые Python может обрабатывать. К счастью, теперь мы знаем о функции eval()
.
Всё приложение будет храниться в скрипте mathrepl.py
. Постепенно мы его заполним необходимым содержимым. Начнем со следующего кода:
Модуль math
мы используем для того, чтобы определить все доступные имена. Три строковые константы применяются для вывода строк в интерфейсе программы. Напишем ключевую функцию нашей программы:
Осталось лишь написать код для взаимодействия с пользователем. В функции main()
мы определяем основной цикл программы для чтения введенных данных и расчета математических выражений, введенных пользователем:
Проверим результат нашей работы:
Вот и всё – наш обработчик математических выражений готов! В случае ошибок при вводе или математически некорректных выражений мы получаем необходимое пояснение. Для самой обработки введенных данных потребовалось лишь несколько строк и функция eval()
.
Заключение
Итак, вы можете использовать eval()
для выполнения выражений Python из строкового или кодового ввода. Эта встроенная функция полезна, когда вы пытаетесь динамически обновлять выражения Python и хотите избежать проблем с созданием собственного обработчика выражений. Однако пользоваться ей стоит с осторожностью.
Другие наши недавние статьи с подробным разбором различных аспектов стандартной библиотеки Python:
- Всё, что нужно знать о декораторах Python
- Как хранить объекты Python со сложной структурой
- 15 вещей, которые нужно знать о словарях Python