Python предоставляет множество встроенных служебных функций (Built-in Functions), в последней официальной документации Python 3 их перечислено 69.
Большинство функций широко используются, например print(), open() и dir(), в то время как некоторые функции обычно не используются, но могут играть необычную роль в определенных сценариях. Встроенные функции можно «поднять», а значит, они уникальны и полезны.
Таким образом, освоение встроенных функций стало навыком, который мы должны осветить.
существует"Python Advanced: как преобразовать строковые константы в переменные?«В этой статье я упомянул eval() и exec(), но мало о них знал. Чтобы компенсировать это знание, я выучил его заново. Эта статья представляет собой сверхподробный отчет об исследовании, в котором систематически, всесторонне и глубоко анализируются эти две функции.
1. Базовое использование eval
Синтаксис: оцен(expression, globals=None, locals=None)
Он имеет три параметра, из которых выражение представляет собой выражение строкового типа или объект кода, используемый для операции; глобальные и локальные параметры являются необязательными параметрами, а значение по умолчанию — None.
В частности, выражение может быть только одним выражением и не поддерживает сложную логику кода, такую как операции присваивания, операторы цикла и т. д. (PS: отдельное выражение не означает «просто и безобидно», см. раздел 4 ниже)
globals используется для указания глобального пространства имен во время выполнения.Тип - словарь.По умолчанию используется встроенное пространство имен текущего модуля. locals указывает локальное пространство имен во время выполнения, тип — словарь, а значение globals используется по умолчанию. Когда оба значения по умолчанию, выполняется область выполнения функции eval. Стоит отметить, что эти два не представляют настоящих пространств имен, они работают только во время операций, а после операций уничтожаются.
x = 10
def func():
y = 20
a = eval('x + y')
print('a: ', a)
b = eval('x + y', {'x': 1, 'y': 2})
print('x: ' + str(x) + ' y: ' + str(y))
print('b: ', b)
c = eval('x + y', {'x': 1, 'y': 2}, {'y': 3, 'z': 4})
print('x: ' + str(x) + ' y: ' + str(y))
print('c: ', c)
func()
Выходной результат:
a: 30
x: 10 y: 20
b: 3
x: 10 y: 20
c: 4
Видно, что когда указано пространство имен, переменная будет искаться в соответствующем пространстве имен. Кроме того, их значения не переопределяют значения в фактическом пространстве имен.
2. Базовое использование exec
Синтаксис: исполнить(object[, globals[, locals]])
В Python 2 exec был оператором, а Python 3 превратил его в функцию, как и print. exec() очень похож на eval(), а значения и функции трех параметров аналогичны.
Основное отличие состоит в том, что первый параметр exec() является не выражением, а блоком кода, что означает две вещи: во-первых, он не может выполнять оценку выражения и возвращать его, а во-вторых, он может выполнять сложную логику кода. , является относительно более мощным, например, когда в блоке кода назначается новая переменная, переменнаявозможныйВыжить в пространствах имен вне функций.
>>> x = 1
>>> y = exec('x = 1 + 1')
>>> print(x)
>>> print(y)
2
None
Видно, что пространство имен внутри и снаружи exec() одинаковое, и переменные передаются оттуда, в отличие от функции eval(), которая требует переменную для получения результата выполнения функции.
3. Анализ некоторых деталей
Обе функции мощные, и они выполняют содержимое строки как допустимый код. ЭтоСобытия, управляемые строкой,Значительный. Однако в процессе реального использования есть много мелких деталей, вот несколько моментов, которые мне известны.
Обычное использование: преобразование строк в соответствующие объекты, такие как строка в список, строка в словарь, строка в кортеж и т. д.
>>> a = "[[1,2], [3,4], [5,6], [7,8], [9,0]]"
>>> print(eval(a))
[[1, 2], [3, 4], [5, 6], [7, 8], [9, 0]]
>>> a = "{'name': 'Python猫', 'age': 18}"
>>> print(eval(a))
{'name': 'Python猫', 'age': 18}
# 与 eval 略有不同
>>> a = "my_dict = {'name': 'Python猫', 'age': 18}"
>>> exec(a)
>>> print(my_dict)
{'name': 'Python猫', 'age': 18}
Возвращаемое значение функции eval() является результатом выполнения ее выражения. В некоторых случаях оно будет равно None, например, когда выражение является оператором print() или операцией append() списка. Результат — None, поэтому возвращаемое значение eval() также будет None.
>>> result = eval('[].append(2)')
>>> print(result)
None
Возвращаемое значение функции exec() будет только None, что не имеет ничего общего с результатом выполнения оператора, поэтому нет необходимости назначать функцию exec(). Выполненные операторы, содержащие return или yield, не производят значений, которые будут работать вне функции exec.
>>> result = exec('1 + 1')
>>> print(result)
None
Параметры globals и locals в этих двух функциях играют роль белого списка.Ограничение области пространства имен предотвращает злоупотребление данными в этой области.
Скомпилированный объект кода функции compile(), который можно использовать в качестве первого параметра eval и exec. compile() тоже волшебная функция, последняя статья, которую я перевел "Операция Python Sao: динамическое определение функций" демонстрирует работу динамически определяемой функции.
Парадоксальное локальное пространство имен: как упоминалось ранее, переменные в функции exec() могут изменять исходное пространство имен, но есть и исключения.
def foo():
exec('y = 1 + 1\nprint(y)')
print(locals())
print(y)
foo()
Согласно предыдущему пониманию, ожидаемый результат состоит в том, что переменная y будет сохранена в локальной переменной, поэтому результат обоих выводов будет равен 2, но фактический результат таков:
2
{'y': 2}
Traceback (most recent call last):
...(略去部分报错信息)
print(y)
NameError: name 'y' is not defined
Очевидно, что в локальном пространстве имен есть переменная y, почему сообщается, что она не определена?
Причина связана с компилятором Python. Для приведенного выше кода компилятор сначала разберет функцию foo в ast (абстрактное синтаксическое дерево), а затем сохранит все узлы переменных в стеке. В это время параметр exec( ) — это просто строка, все это константа и не выполняется как код, поэтому y еще не существует. Пока не проанализирована вторая функция print(), когда переменная y появляется впервые, но из-за отсутствия полного определения y не сохраняется в локальном пространстве имен.
Во время выполнения функция exec() динамически создает локальную переменную y , но, поскольку механизм реализации Python "Локальное пространство имен во время выполнения является неизменным», то есть y никогда не может стать членом локального пространства имен в это время, и при выполнении print() сообщается об ошибке.
Что касается того, почему результат, полученный с помощью locals(), имеет y, почему он не может представлять реальное локальное пространство имен? Почему нельзя динамически изменять локальные пространства имен? Вы можете просмотреть мой предыдущий пост "Подводные камни динамического назначения Python", кроме того, на официальном сайте бага также есть обсуждение этого вопроса, проверьте адрес:bugs.python.org/issue4831
Если вы хотите удалить y после выполнения exec(), вы можете сделать это:z = locals()['y']
, однако, если вы случайно напишете следующий код, будет сообщено об ошибке:
def foo():
exec('y = 1 + 1')
y = locals()['y']
print(y)
foo()
#报错:KeyError: 'y'
#把变量 y 改为其它变量则不会报错
KeyError
Это означает, что соответствующий ключ не существует в словаре. В этом примере объявлено y, но присваивание не может быть выполнено из-за циклической ссылки, то есть значение, соответствующее значению ключа, является недопустимым значением, поэтому, если оно не может быть прочитано, будет сообщено об ошибке.
Есть еще 4 варианта этого примера, и я пытался объяснить их по-человечески, но пытался долго и безуспешно. Я оставлю это вам позже, и я напишу отдельную статью, когда разберусь с этим.
4. Почему eval() следует использовать с осторожностью?
Многие языки динамического программирования имеют функцию eval(), которая делает то же самое, но все без исключения люди будут советовать вам избегать ее.
Почему следует использовать eval() с осторожностью? В основном из соображений безопасности для ненадежных источников данных функция eval может вызвать проблемы с внедрением кода.
>>> eval("__import__('os').system('whoami')")
desktop-fa4b888\pythoncat
>>> eval("__import__('subprocess').getoutput('ls ~')")
#结果略,内容是当前路径的文件信息
В приведенном выше примере мои личные данные были раскрыты. И что еще страшнее, если вы измените команду наrm -rf ~
, все файлы в текущем каталоге будут безошибочно удалены.
Для приведенного выше примера существует ограниченный способ указать глобальные переменные как{'__builtins__': None}
или{'__builtins__': {}}
.
>>> s = {'__builtins__': None}
>>> eval("__import__('os').system('whoami')", s)
#报错:TypeError: 'NoneType' object is not subscriptable
__builtins__
Содержит имена из встроенного пространства имен. Введите dir(__builtins__) в консоли, чтобы найти имена многих встроенных функций, исключений и других атрибутов. По умолчанию параметр globals функции eval неявно передается__builtins__
, даже если параметр globals равен {} , поэтому, если вы хотите отключить его, вам нужно явно указать его значение.
Приведенный выше пример сопоставляет его со значением None, что означает, что встроенное пространство имен, доступное для eval, ограничено значением None, что ограничивает возможность выражений вызывать встроенные модули или атрибуты.
Однако этот метод не является надежным, так как все еще существуют способы запуска атаки.
Некий исследователь уязвимостей поделился в своем блоге идеей, которая открыла глаза. Основной код — это следующее предложение, вы можете попробовать выполнить его и посмотреть, что получится на выходе.
>>> ().__class__.__bases__[0].__subclasses__()
Объяснение этого кода и другие методы использования см. в блоге. (адрес:Уууууууууууууууууууууууу.com/articles/amount…
Существует также блог, в котором не только упоминается метод из приведенного выше примера, но и предлагается новая идея:
#警告:千万不要执行如下代码,后果自负。
>>> eval('(lambda fc=(lambda n: [c 1="c" 2="in" 3="().__class__.__bases__[0" language="for"][/c].__subclasses__() if c.__name__ == n][0]):fc("function")(fc("code")(0,0,0,0,"KABOOM",(),(),(),"","",0,""),{})())()', {"__builtins__":None})
Эта строка кода вызовет прямой сбой Python. Конкретный анализ находится в:сегмент fault.com/ah/119000001…
Помимо средств хакеров, простой контент также может запускать атаки. Написание, подобное приведенному ниже примеру, за короткое время исчерпает вычислительные ресурсы сервера.
>>> eval("2 ** 888888888", {"__builtins__":None}, {})
Как упоминалось выше, мы наглядно продемонстрировали опасность функции eval(), однако даже при осторожном использовании ее экспертами по Python нет никакой гарантии, что она не пойдет не так, как надо.
В официальном модуле dumpdbm была обнаружена уязвимость безопасности (2014 г.), злоумышленник мог запустить атаку при вызове eval() путем подделки файла базы данных. (Подробности:не говори. Python.org/issue22885)
По совпадению, в прошлом месяце (2019.02) основной разработчик также поднял проблему безопасности для Python 3.8, предложив не использовать функцию eval() в logging.config, и проблема все еще остается открытой. (Подробности:не говори. Python.org/issue36022)
Всего этого достаточно, чтобы объяснить, почему eval() следует использовать с осторожностью. По той же причине следует с осторожностью использовать функцию exec().
5. Безопасные альтернативы
Поскольку существуют всевозможные риски безопасности, зачем создавать эти два встроенных метода? Зачем их использовать?
Причина проста, потому что Python — гибкий и динамичный язык. В отличие от статических языков, динамические языки поддерживают генерацию динамического кода.Для уже развернутых проектов исправления ошибок также могут быть реализованы с небольшими локальными изменениями.
Так есть ли способ использовать их относительно безопасно?
Восточный модульliteral()
является безопасной альтернативой eval(), в отличие от eval(), которая выполняется без проверки, ast.literal() сначала проверяет, является ли содержимое выражения допустимым или нет. Литералы, которые он допускает, следующие:
строки, байты, числа, кортежи, списки, словари, наборы, логические значения и None
Если содержание является незаконным, будет сообщено об ошибке:
import ast
ast.literal_eval("__import__('os').system('whoami')")
报错:ValueError: malformed node or string
Однако у него есть и недостатки: компилятор AST имеет ограниченную глубину стека, что может привести к сбою программы, когда содержимое анализируемой строки слишком велико или слишком сложно.
Что касается exec() , похоже, что похожей альтернативы не существует, поскольку то, что он может поддерживать сам по себе, более сложное и разнообразное.
Наконец, предложение: выяснить их различия и операционные детали (например, предыдущее содержимое локального пространства имен), использовать их с осторожностью, ограничить доступные пространства имен и полностью проверить источник данных.
Связанное чтение:
Подводные камни динамического назначения Python
Операция Python Sao: динамическое определение функций
Python Advanced: как преобразовать строковые константы в переменные?
docs.Python.org/3/library/… ах…
публика【Питон кот], уделяя особое внимание технологии Python, науке о данных и глубокому обучению, пытаясь создать интересную и полезную платформу для обмена знаниями. В этом выпуске публикуются высококачественные серии статей, в том числе серия Meow Star Philosophy Cat, расширенная серия Python, серия рекомендаций по хорошим книгам, высококачественные рекомендации и перевод на английский язык и т. д. Добро пожаловать, обратите внимание. PS: фоновый ответ"люблю учиться” и получите бесплатный обучающий пакет.