Динамическое выполнение выражений в Python: eval()

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту
Архив новостей

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


2020-05-15 18:01

разработка по

Функция eval() полезна, когда необходимо выполнить динамически обновляемое выражение Python из какого-либо ввода (например, функции input()), представленного в виде строки или объекта байт-кода. Это невероятно полезный инструмент, но то, что она может выполнять программный код, имеет важные последствия для безопасности, которые следует учесть перед ее применением.

Статья является сокращенным переводом публикации Леоданиса Посо Рамоса Python eval(): Evaluate Expressions Dynamically. Из этого руководства вы узнаете:

  • Как работает eval().
  • Как использоватьeval() для динамического выполнения кода.
  • Как минимизировать риски для безопасности, связанные с использованиемeval().

Примечание

Если вы умеете пользоваться блокнотами Jupyter, текст также адаптирован в виде ipynb-файла, доступного в GitHub-репозитории. Файл можно запустить в интерактивно, ничего не устанавливая, в Colab.

Разбираемся в том, как работает eval()

Вы можете использовать встроеннyю функцию eval() для динамического исполнения выражений из ввода на основе строки или скомпилированного кода. Если вы передаете вeval()строку, то функция анализирует ее, компилирует вбайт-коди выполняет как выражение Python.

Сигнатураeval() определена следующим образом:

             eval(expression[, globals[, locals]])         

Первый аргумент expressionсодержит выражение, которое необходимо выполнить. Функция также принимает два необязательных аргумента globalsи locals, о которых мы поговорим в соответствующих разделах. Начнём по порядку – с аргументаexpression.

Примечание

Для динамического выполнения кода можно также использовать функцию exec(). Основное различие между eval() и exec() состоит в том, что eval() может выполнять лишь выражения, тогда как функции exec() можно «скормить» любой фрагмент кода Python.

Первый аргумент: expression

Когда мы вызываем eval(), содержание expression воспринимается интерпретатором как выражение Python. Посмотрите на следующие примеры, принимающие строковый ввод:

             >>> eval("2 ** 8") 256 >>> eval("1024 + 1024") 2048 >>> eval("sum([8, 16, 32])") 56 >>> x = 100 >>> eval("x * 2") 200         

При вызовеeval() со строковым выражением в качестве аргумента, функция возвращает значение, полученное в результате оценки входной строки. По умолчаниюeval() имеет доступ к глобальным именам, таким как xв приведенном выше примере.

Чтобы оценить строковое выражение,eval() выполняет следующую последовательность действий:

  1. Парсинг выражения.
  2. Компилирование в байт-код.
  3. Выполнение кода выражения Python.
  4. Возвращение результата.

Имя аргумента expression подчеркивает, что функция работает только с выражениями, но не составными конструкциями. При попытке передачи блока кода вместо выражения будет получено исключение SyntaxError:

             >>> x = 100 >>> eval("if x: print(x)") SyntaxError: invalid syntax          

Таким образом, вeval() нельзя передать конструкции c if, import, def или class, с циклами for и while. Однако ключевое слово forможет использоваться вeval()в случае выражений для генераторов списков.

В eval()запрещены и операции присваивания:

             >>> eval("pi = 3.1416") SyntaxError: invalid syntax         

SyntaxError также вызывается в случаях, когда eval() не удается распарсить выражение из-за ошибки в записи:

             >>> eval("5 + 7 *") SyntaxError: unexpected EOF while parsing         

В eval() можно передавать объекты кода (code objects). Чтобы скомпилировать код, который вы собираетесь передатьeval(), можно использоватьcompile(). Это встроенная функция, которая может компилировать строку в объект кода илиAST-объект.

Детали того, как использовать compile(), выходят за рамки этого руководства, но здесь мы кратко рассмотрим первые три обязательных аргумента:

  1. source содержит исходный код, который необходимо скомпилировать. Этот аргумент принимает обычные строки, байтовые строки и объекты AST.
  2. filename определяет файл, из которого прочитан код. Если используется строчный ввод, значение аргумента должно быть равно строке <string>.
  3. mode указывает, какой тип объекта кода мы хотим получить. Если нужно обработать код с помощью eval(), в качестве значения аргумента указывается "eval"

Примечание

Для лучшего ознакомления воспользуйтесь официальной документацией функции compile().

Таким образом, мы можем использовать compile() для предоставления объектов кода вeval() вместо обычных строк:

             >>> code = compile("5 + 4", "<string>", "eval") >>> eval(code) 9 >>> import math >>> code = compile("4 / 3 * math.pi * math.pow(25, 3)", "<string>", "eval") >>> eval(code) 65449.84694978735         

Использование объектов кода полезно при многократном вызове. Если мы предварительно скомпилируем входное выражение, то последующие вызовыeval() будут выполняться быстрее, так как не будут повторяться шаги синтаксического анализа и компиляции.

Второй аргумент: globals

Аргумент globalsопционален. Он содержит словарь, обеспечивающий доступeval()к глобальному пространству имен. С помощью глобальных переменных можно указатьeval(), какие глобальные имена использовать при выполнении выражения.

Глобальные имена – это все те имена, которые доступны в текущей глобальной области или пространстве имен. Вы можете получить к ним доступ из любого места в вашем коде.

Все имена, переданные глобальным переменным в словаре, будут доступны eval()во время выполнения.

             >>> x = 100  # Глобальная переменная >>> eval("x + 100", {"x": x}) 200 >>> y = 200  # Другая глобальная переменная >>> eval("x + y", {"x": x}) NameError: name 'y' is not defined         

Любые глобальные имена, определенные вне пользовательского словаря globals, не будут доступны изнутри eval(), будет вызвано исключение NameError.

             >>> eval("x + y", {"x": x, "y": y}) 300         

Вы также можете указать имена, которых нет в текущей глобальной области видимости. Чтобы это работало, нужно указать конкретное значение для каждого имени. Тогда eval() будет интерпретировать эти имена, как если бы это были глобальные переменные:

             >>> eval("x + y + z", {"x": x, "y": y, "z": 300}) 600 >>> z  # самой переменной нет в глобальной области видимости NameError: name 'z' is not defined         

Если вы предоставите eval() пользовательский словарь, который не содержит значения для ключа "__builtins__", то ссылка на словарь встроенных функций всё равно будет автоматически добавлена к ключу "__builtins__", прежде чем выражение будет проанализировано. Это гарантирует, что eval() имеет полный доступ ко всем встроенным именам Python при оценке выражения.

             >>> eval("sum([2, 2, 2])", {}) 6 >>> eval("min([1, 2, 3])", {}) 1 >>> eval("pow(10, 2)", {}) 100         

Несмотря на переданный пустой словарь ({}),eval() имеет доступ к встроенным функциям.

При вызовеeval()без передачи пользовательского словаря в глобальные переменные аргумент по умолчанию будет использовать словарь, возвращаемыйglobals() в среде, где вызываетсяeval():

             >>> x = 100  # Глобальная переменная >>> y = 200  # Другая глобальная переменная >>> eval("x + y") 300         

Таким образом, передача словаря в аргументе globalsслужит как способ намеренно ограничить область видимость имен для функции eval().

Третий аргумент: locals

Аргумент locals также является необязательным аргументом. В этом случае словарь содержит переменные, которыеeval()использует в качестве локальных имен при оценке выражения.

Локальными называются те имена (переменные, функции, классы и т.д.), которые мы определяем внутри данной функции. Локальные имена видны только изнутри включающей функции.

             >>> eval("x + 100", {}, {"x": 100}) 200 >>> eval("x + y", {}, {"x": 100}) NameError: name 'y' is not defined         

Обратите внимание, что для передачи словаря locals сначала необходимо предоставить словарь для globals. Передача по ключу в случае eval() не работает:

             >>> eval("x + 100", locals={"x": 100}) TypeError: eval() takes no keyword arguments         

Главное практическое различие между globals и locals заключается в том, что Python автоматически вставит ключ "__builtins__" в globals, если этот ключ еще не существует. Cловарь locals остается неизменным во время выполнения eval().

Выполнение выражений с eval()

Функцияeval() используется, когда нужно динамически изменять выражения, а применение других техник и инструментов Python требует избыточных усилий. В этом разделе мы обсудим, как использоватьeval()для булевых, математических и прочих выражений Python.

Булевы выражения

Булевы выражения – это выражения Python, которые возвращают логическое значение. Обычно они используются для проверки, является ли какое-либо условие истинным или ложным:

             >>> x = 100 >>> y = 100 >>> eval("x != y") False >>> eval("x < 200 and y > 100") False >>> eval("x is y") True >>> eval("x in {50, 100, 150, 200}") True         

Зачем же может потребоваться использовать eval() вместо непосредственного применения логического выражения? Предположим, нам нужно реализовать условный оператор, но вы хотите на лету менять условие:

             def func(a, b, condition):     if eval(condition):         return a + b     return a - b         
             >>> func(2, 4, "a > b") -2 >>> func(2, 4, "a < b") 6 >>> func(2, 2, "a is b") 4         

Внутри func()для оценки предоставленного условия используется функцияeval(), возвращающая a+bили a-bв соответствии с результатом оценки.

Теперь представьте, как бы вы реализовали то же поведение безeval()для обработки любого логического выражения.

Математические выражения

Один из распространенных вариантов использованияeval() в Python – оценка математических выражений из строкового ввода. Например, если вы хотите создать калькулятор на Python, вы можете использовать eval(), чтобы оценить вводимые пользователем данные и вернуть результат вычислений:

             >>> eval("5 + 7") 12 >>> eval("(5 + 7) / 2") 6.0 >>> import math >>> eval("math.sqrt(math.pow(10, 2) + math.pow(15, 2))") 18.027756377319946         

Выражения общего вида

Вы можете использоватьeval()и с более сложными выражениями Python, включающими вызовы функций, создание объектов, доступ к атрибутам и т. д.

Например, можно вызвать встроенную функцию или функцию, импортированную с помощью стандартного или стороннего модуля. В следующих примерахeval() используется для запуска различных системных команд.

             >>> import subprocess >>> # Запуск команды echo >>> eval("subprocess.getoutput('echo Hello, World')") 'Hello, World' >>> # Запуск Firefox (если он установлен) >>> eval("subprocess.getoutput('firefox')")         

Таким образом, можно передавать команды через какой-либо строковый интерфейс (например, форму в браузере) и выполнять код Python.

Однако по той же причинеeval()может подвергнуть нас серьезным угрозам безопасности, например, позволит злоумышленнику запускать системные команды или выполнять кода на нашем компьютере. В следующем разделе мы обсудим способы устранения угроз безопасности, связанных сeval().

Минимизация проблем безопасности, связанных с eval()

В связи с проблемами безопасности обычно рекомендуется по возможности не использоватьeval(). Но если вы решили, что функция необходима, простое практическое правило состоит в том, чтобы никогда не использовать ее для любого ввода, которому нельзя доверять. Сложность в том, чтобы выяснить, каким видам ввода доверять можно .

В качестве примера того, как безответственное использованиеeval() может сделать ваш код небезопасным, предположим, что вы хотите создать онлайн-сервис для оценки произвольных выражений Python. Ваш пользователь вводит выражения и нажимает кнопку «Выполнить». Приложение получает пользовательский ввод и передает его для выполнения вeval().

Если вы используете Linux и приложения имеет необходимые разрешения, то злонамеренный пользователь может ввести опасную строку, подобную следующей:

             "__import__('subprocess').getoutput('rm –rf *')"         

Выполнение выражения удалит все файлы в текущей директории.

Примечание

__import__() – это встроенная функция, которая принимает имя модуля в виде строки и возвращает ссылку на объект модуля. __import__() – это функция, которая полностью отличается от оператора import. Как мы упоминали выше, вы не можете вызвать оператор импорта с помощью eval().

Ограничение globals и locals

Вы можете ограничить среду выполненияeval(), задавая собственные словари аргументам globals и locals. Например, пустые словари для обоих аргументов, чтобыeval()не мог получить доступ к именам в текущей области или пространстве имен вызывающей стороны:

             >>> x = 100 >>> eval("x * 5", {}, {}) NameError: name 'x' is not defined         

К сожалению, это ограничение не устраняет другие проблемы безопасности, связанные с использованиемeval(), поскольку остается доступ ко всем встроенным именам и функциям Python.

Ограничение __builtins__

Как мы видели ранее, перед синтаксическим анализом выражения eval() автоматически вставляет ссылку на словарь __builitins__ в globals. Злоумышленник может использовать это поведение, используя встроенную функцию __import__(), чтобы получить доступ к стандартной библиотеке или любому стороннему модулю, установленному в системе:

             >>> eval("__import__('math').sqrt(25)", {}, {}) 5.0 >>> eval("__import__('subprocess').getoutput('echo Hello, World')", {}, {}) 'Hello, World'         

Чтобы минимизировать риски, можно переопределить __builtins__ в globals:

             >>> eval("__import__('math').sqrt(25)", {"__builtins__": {}}, {}) NameError: name '__import__' is not defined         

Ограничение имён во входных данных

Однако даже после таких ухищрений Python останется уязвим. Например, можно получить доступ к объекту класса, используя литерал типа, например "", [], {} или (), а также некоторые специальные атрибуты:

             >>> "".__class__.__base__ object >>> [].__class__.__base__ object         

Получив доступ к объекту, можно использовать специальный метод .__subclasses__(), чтобы получить доступ ко всем классам, которые наследованы объектом. Вот как это работает:

             for sub_class in ().__class__.__base__.__subclasses__():     print(sub_class.__name__)         
             type weakref weakcallableproxy weakproxy int bytearray bytes list ...         

Этот код напечатает большой список классов. Некоторые из этих классов довольно мощные и могут быть чрезвычайно опасны в чужих руках. Это открывает еще одну важную дыру в безопасности, которую вы не сможете закрыть, просто ограничивая окружение eval():

             input_string = """[     c for c in ().__class__.__base__.__subclasses__()     if c.__name__ == "range"     ][0](10)"""         
             >>> list(eval(input_string, {"__builtins__": {}}, {})) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]         

Генератор списка в приведенном коде фильтрует классы объекта, чтобы вернуть список, содержащий класс range. Далее range вызывается для создания соответствующего объекта. Это хитрый способ обойти исключение TypeError, вызываемое в результате ограничения "__builtins__".

             >>> list(eval(range(10), {"__builtins__": {}}, {})) TypeError: eval() arg 1 must be a string, bytes or code object         

Возможное решение этой уязвимости состоит в том, чтобы ограничить использование имен во входных данных набором безопасных имен либо исключить всякое использование любых имен.

Чтобы реализовать эту технику, необходимо выполнить следующие шаги:

  1. Создать словарь, содержащий имена, которые могут использоваться в eval().
  2. Скомпилировать входную строку в байт-код, используя compile() в режиме eval.
  3. Проверить .co_names в объекте байт-кода, чтобы убедиться, что он содержит только разрешенные имена.
  4. Вызвать исключение NameError, если пользователь пытается использовать недопустимое имя.

Взглянем на следующую функцию, в которой реализованы все эти шаги:

             def eval_expression(input_string):     allowed_names = {"sum": sum}     code = compile(input_string, "<string>", "eval")     for name in code.co_names:         if name not in allowed_names:             raise NameError(f"Использование {name} не разрешено.")     return eval(code, {"__builtins__": {}}, allowed_names)         

Эта функция ограничивает имена, которые можно использовать в eval(), именами в словаре allowed_names. Для этого функция использует .co_names – атрибут объекта кода, содержащий кортеж имен в объекте кода.

Следующие примеры показывают, как написанная нами функция eval_expression() работает на практике:

             >>> eval_expression("3 + 4 * 5 + 25 / 2") 35.5 >>> eval_expression("sum([1, 2, 3])") 6 >>> eval_expression("pow(10, 2)") NameError: Использование pow не разрешено.         

Если нужно полностью запретить применение имен, достаточно переписать eval_expression() следующим образом:

             def eval_expression(input_string):     code = compile(input_string, "<string>", "eval")     if code.co_names:         raise NameError(f"Использование имён запрещено.")     return eval(code, {"__builtins__": {}}, {})         
             >>> eval_expression("3 + 4 * 5 + 25 / 2") 35.5 >>> eval_expression("sum([1, 2, 3])") NameError: Использование имён запрещено.         

Ограничение входных данных до литералов

Типичный пример использования eval() в Python – это выполнение выражений, содержащих стандартные литералы Python. Задача настолько распространенная, что стандартная библиотека предоставляет соответствующую функцию literal_eval(). Функция не поддерживает операторы, но работает со списками, кортежами, числами, строками и т. д.:

             >>> from ast import literal_eval >>> literal_eval("15.02") 15.02 >>> literal_eval("[1, 15]") [1, 15] >>> literal_eval("{'one': 1, 'two': 2}") {'one': 1, 'two': 2} >>> literal_eval("sum([1, 15]) + 5 + 8 * 2") ValueError: malformed node or string: ...         

Использование eval() совместно с input()

В Python 3.x встроенная функция input() читает пользовательский ввод из командной строки, преобразует его в строку, удаляет завершающий символ новой строки и возвращает результат вызывающей стороне. Поскольку результатом input() является строка, ее можно передать в eval() и выполнить как выражение Python:

             >>> eval(input("Введите математическое выражение: ")) Введите математическое выражение: 15*2 30         

Это распространенный вариант использования eval(). Он также эмулирует поведеие input() в версиях Python 2.x, где функции можно было передать строковое выражение для выполнения (впоследствии от этого отказались из соображений безопасности).

Построим обработчик математических выражений

Итак, мы узнали, как работает eval() в Python и как использовать функцию на практике. Мы также выяснили, что eval() имеет важные последствия для безопасности и что обычно считается хорошей практикой избегать использования eval() в коде. Однако в некоторых ситуациях eval() может сэкономить много времени и усилий.

В этом разделе вы напишем приложение для оценки математических выражений на лету. Без использования eval(), нам потребовалось бы выполнить следующие шаги:

  1. Распарсить входное выражение.
  2. Преобразовать компоненты выражения в объекты Python (числа, операторы, функции).
  3. Объединить всё в исполняемое выражение.
  4. Проверить валидность выражения для Python.
  5. Выполнить итоговое выражение и вернуть результат вычислений.

Это потребовало бы большой работы, учитывая разнообразие возможных выражений, которые Python может обрабатывать. К счастью, теперь мы знаем о функции eval().

Всё приложение будет храниться в скрипте mathrepl.py. Постепенно мы его заполним необходимым содержимым. Начнем со следующего кода:

mathrepl.py
             import math  __version__ = "1.0"  ALLOWED_NAMES = {     k: v for k, v in math.__dict__.items() if not k.startswith("__") }  PS1 = "mr>>"  WELCOME = f""" MathREPL {__version__} - обработчик математических выражений на Python! Введите математическое выражение после приглашения "{PS1}". Для дополнительной информации используйте команду help. Чтобы выйти, наберите quit или exit. """  USAGE = f""" Соберите математическое выражение из чисел и операторов. Можно использовать любые из следующих функций и констант:  {', '.join(ALLOWED_NAMES.keys())} """         

Модуль math мы используем для того, чтобы определить все доступные имена. Три строковые константы применяются для вывода строк в интерфейсе программы. Напишем ключевую функцию нашей программы:

mathrepl.py
             def evaluate(expression):     """Вычисляет математическое выражение."""     # Компиляция выражения в байт-код     code = compile(expression, "<string>", "eval")      # Валидация доступных имен     for name in code.co_names:         if name not in ALLOWED_NAMES:             raise NameError(f"The use of '{name}' is not allowed")      return eval(code, {"__builtins__": {}}, ALLOWED_NAMES)         

Осталось лишь написать код для взаимодействия с пользователем. В функции main() мы определяем основной цикл программы для чтения введенных данных и расчета математических выражений, введенных пользователем:

mathrepl.py
             def main():     """Читает и рассчитывает введенное выражение"""     print(WELCOME)     while True:         # Читаем пользовательский ввод         try:             expression = input(f"{PS1} ")         except (KeyboardInterrupt, EOFError):             raise SystemExit()          # Поддержка специальных команд         if expression.lower() == "help":             print(USAGE)             continue         if expression.lower() in {"quit", "exit"}:             raise SystemExit()          # Вычисление выражения и обработка ошибок         try:             result = evaluate(expression)         except SyntaxError:             # Некорректное выражение             print("Вы ввели некорректное выражение.")             continue         except (NameError, ValueError) as err:             # Если пользователь попытался использовать неразрешенное имя             # или неверное значение в переданной функции             print(err)             continue          # Выводим результат, если не было ошибок         print(f"Результат: {result}")         

Проверим результат нашей работы:

Shell
             python3 mathrepl.py  MathREPL 1.0 - обработчик математических выражений на Python! Введите математическое выражение после приглашения "mr>>". Для дополнительной информации используйте команду help. Чтобы выйти, наберите quit или exit.  mr>> 25 * 2 Результат: 50 mr>> sqrt(25) Результат: 5.0 mr>> pi Результат: 3.141592653589793 mr>> 5 * (25 + 4 Вы ввели некорректное выражение. mr>> sum([1, 2, 3, 4, 5]) The use of 'sum' is not allowed mr>> sqrt(-15) math domain error mr>> factorial(-15) factorial() not defined for negative values mr>> exit         

Вот и всё – наш обработчик математических выражений готов! В случае ошибок при вводе или математически некорректных выражений мы получаем необходимое пояснение. Для самой обработки введенных данных потребовалось лишь несколько строк и функция eval().

Заключение

Итак, вы можете использовать eval() для выполнения выражений Python из строкового или кодового ввода. Эта встроенная функция полезна, когда вы пытаетесь динамически обновлять выражения Python и хотите избежать проблем с созданием собственного обработчика выражений. Однако пользоваться ей стоит с осторожностью.

***

Другие наши недавние статьи с подробным разбором различных аспектов стандартной библиотеки Python:

  • Всё, что нужно знать о декораторах Python
  • Как хранить объекты Python со сложной структурой
  • 15 вещей, которые нужно знать о словарях Python

Источники


Источник: proglib.io

Комментарии: