ChatGPT解决这个技术问题 Extra ChatGPT

eval、exec 和 compile 有什么区别?

我一直在研究 Python 代码的动态评估,遇到了 eval()compile() 函数以及 exec 语句。

有人可以解释一下 evalexec 之间的区别,以及 compile() 的不同模式如何适应吗?

这和问:eval$(...) 有什么区别是一样的吗?

A
Antti Haapala -- Слава Україні

简短的回答,或 TL;DR

基本上,eval 用于评估单个动态生成的 Python 表达式,而 exec 仅用于执行动态生成的 Python 代码,因为它的副作用。

evalexec 有以下两个区别:

eval 只接受一个表达式,exec 可以接受一个包含 Python 语句的代码块:循环、try:except:、类和函数/方法定义等等。 Python 中的表达式是您在变量赋值中可以拥有的任何值:a_variable =(您可以放在这些括号内的任何内容都是表达式) eval 返回给定表达式的值,而 exec 忽略其代码中的返回值,并且总是返回 None (在 Python 2 中,它是一个语句,不能用作表达式,所以它真的不返回任何东西)。

在 1.0 - 2.7 版本中,exec 是一个语句,因为 CPython 需要为使用 exec 的函数生成不同类型的代码对象,因为它在函数内部产生副作用。

在 Python 3 中,exec 是一个函数;它的使用对使用它的函数的编译字节码没有影响。

因此基本上:

>>> a = 5
>>> eval('37 + a')   # it is an expression
42
>>> exec('37 + a')   # it is an expression statement; value is ignored (None is returned)
>>> exec('a = 47')   # modify a global variable as a side effect
>>> a
47
>>> eval('a = 47')  # you cannot evaluate a statement
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1
    a = 47
      ^
SyntaxError: invalid syntax

'exec' 模式下的 compile 将任意数量的语句编译成隐式始终返回 None 的字节码,而在 'eval' 模式下它将 single 表达式编译成 返回的字节码 该表达式的值。

>>> eval(compile('42', '<string>', 'exec'))  # code returns None
>>> eval(compile('42', '<string>', 'eval'))  # code returns 42
42
>>> exec(compile('42', '<string>', 'eval'))  # code returns 42,
>>>                                          # but ignored by exec

'eval' 模式下(如果传入字符串,则使用 eval 函数),如果源代码包含语句或超出单个表达式的任何其他内容,则 compile 引发异常:

>>> compile('for i in range(3): print(i)', '<string>', 'eval')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1
    for i in range(3): print(i)
      ^
SyntaxError: invalid syntax

实际上,语句 “eval 只接受一个表达式” 仅适用于将字符串(包含 Python 源代码)传递给 eval 时。然后使用 compile(source, '<string>', 'eval') 在内部将其编译为字节码,这才是真正不同的地方。

如果将 code 对象(包含 Python 字节码)传递给 execeval它们的行为相同,除了 exec 忽略的事实返回值,始终返回 None。因此,可以使用 eval 来执行包含语句的内容,如果您只是在 compile 之前将其转换为字节码而不是将其作为字符串传递:

>>> eval(compile('if 1: print("Hello")', '<string>', 'exec'))
Hello
>>>

即使编译的代码包含语句,也可以正常工作。它仍然返回 None,因为这是从 compile 返回的代码对象的返回值。

'eval' 模式下(如果传入字符串,则使用 eval 函数),如果源代码包含语句或超出单个表达式的任何其他内容,则 compile 引发异常:

>>> compile('for i in range(3): print(i)', '<string>'. 'eval')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1
    for i in range(3): print(i)
      ^
SyntaxError: invalid syntax

更长的答案,也就是血淋淋的细节

执行和评估

exec 函数(原为 a statement in Python 2)用于执行动态创建的语句或程序:

>>> program = '''
for i in range(3):
    print("Python is cool")
'''
>>> exec(program)
Python is cool
Python is cool
Python is cool
>>> 

eval 函数对 single expression 执行相同的操作, 返回表达式的值:

>>> a = 2
>>> my_calculation = '42 * a'
>>> result = eval(my_calculation)
>>> result
84

execeval 都接受程序/表达式作为包含源代码的 strunicodebytes 对象,或作为 code 对象包含 Python 字节码。

如果将包含源代码的 str/unicode/bytes 传递给 exec,则其行为等同于:

exec(compile(source, '<string>', 'exec'))

eval 的行为类似地等同于:

eval(compile(source, '<string>', 'eval'))

由于所有表达式都可以用作 Python 中的语句(这些在 Python abstract grammar 中称为 Expr 节点;反之则不然),如果您不需要返回值,则始终可以使用 exec。也就是说,您可以使用 eval('my_func(42)')exec('my_func(42)'),区别在于 eval 返回 my_func 返回的值,而 exec 将其丢弃:

>>> def my_func(arg):
...     print("Called with %d" % arg)
...     return arg * 2
... 
>>> exec('my_func(42)')
Called with 42
>>> eval('my_func(42)')
Called with 42
84
>>> 

在这 2 个中,只有 exec 接受包含语句(如 defforwhileimportclass)、赋值语句(又名 a = 42)或整个程序的源代码:

>>> exec('for i in range(3): print(i)')
0
1
2
>>> eval('for i in range(3): print(i)')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1
    for i in range(3): print(i)
      ^
SyntaxError: invalid syntax

execeval 都接受 2 个额外的位置参数 - globalslocals - 这是代码看到的全局和局部变量范围。这些默认为 execeval 范围内的 globals()locals(),但任何字典都可以用于 globals,任何 mapping 都可以用于 locals(当然包括 dict )。这些不仅可用于限制/修改代码看到的变量,还经常用于捕获execute 代码创建的变量:

>>> g = dict()
>>> l = dict()
>>> exec('global a; a, b = 123, 42', g, l)
>>> g['a']
123
>>> l
{'b': 42}

(如果您显示整个 g 的值,它会更长,因为 execeval 如果缺少内置模块,则会自动将其作为 __builtins__ 添加到全局变量中)。

在 Python 2 中,exec 语句的官方语法实际上是 exec code in globals, locals,如

>>> exec 'global a; a, b = 123, 42' in g, l

然而,替代语法 exec(code, globals, locals) 也一直被接受(见下文)。

编译

通过预先将源代码编译为 code 对象,内置 compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1) 可用于加快使用 execeval 重复调用相同代码的速度。 mode 参数控制 compile 函数接受的代码片段类型及其生成的字节码类型。选项有 'eval''exec''single'

'eval' 模式需要一个表达式,并且会生成字节码,运行时将返回该表达式的值: >>> dis.dis(compile('a + b', '', 'eval')) 1 0 负载名称 0 (a) 3 负载名称 1 (b) 6 BINARY_ADD 7 RETURN_VALUE

'exec' 接受从单个表达式到整个代码模块的任何类型的 python 构造,并像执行模块顶级语句一样执行它们。代码对象返回无: >>> dis.dis(compile('a + b', '', 'exec')) 1 0 LOAD_NAME 0 (a) 3 LOAD_NAME 1 (b) 6 BINARY_ADD 7 POP_TOP < - 丢弃结果 8 LOAD_CONST 0 (None) <- 在堆栈上加载 None 11 RETURN_VALUE <- 返回堆栈顶部

'single' 是 'exec' 的一种有限形式,它接受包含单个语句(或由 ; 分隔的多个语句)的源代码,如果最后一个语句是表达式语句,则生成的字节码也会打印该表达式值的 repr到标准输出(!)。 if-elif-else 链、带有 else 的循环以及带有其 except、else 和 finally 块的 try 被视为单个语句。包含 2 个顶级语句的源代码片段对于“单个”来说是错误的,除了在 Python 2 中存在有时允许代码中有多个顶级语句的错误;只有第一个被编译;其余的被忽略: 在 Python 2.7.8 中: >>> exec(compile('a = 5\na = 6', '', 'single')) >>> a 5 And in Python 3.4.2 : >>> exec(compile('a = 5\na = 6', '', 'single')) Traceback(最近一次调用最后一次):文件“”,第 1 行,在 文件“”,第 1 行 a = 5 ^ SyntaxError: multiple statements found while compile a single statement 这对于制作交互式 Python shell 非常有用。但是,即使您评估结果代码,也不会返回表达式的值。

因此 execeval 的最大区别实际上来自 compile 函数及其模式。

除了将源代码编译为字节码外,compile 还支持将 abstract syntax trees(Python 代码的解析树)编译为 code 对象;并将源代码转换为抽象语法树(ast.parse 是用 Python 编写的,只调用 compile(source, filename, mode, PyCF_ONLY_AST));例如,这些用于动态修改源代码,也用于动态代码创建,因为在复杂情况下,将代码作为节点树而不是文本行来处理通常更容易。

虽然 eval 仅允许您评估包含单个表达式的字符串,但您可以 eval 整个语句,甚至是compile已转换为字节码的整个模块;也就是说,对于 Python 2,print 是一个语句,不能直接eval引导:

>>> eval('for i in range(3): print("Python is cool")')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1
    for i in range(3): print("Python is cool")
      ^
SyntaxError: invalid syntax

compile'exec' 模式将其转换为 code 对象,您可以evaleval 函数将返回 None

>>> code = compile('for i in range(3): print("Python is cool")',
                   'foo.py', 'exec')
>>> eval(code)
Python is cool
Python is cool
Python is cool

如果查看 CPython 3 中的 evalexec 源代码,这一点非常明显;它们都使用相同的参数调用 PyEval_EvalCode,唯一的区别是 exec explicitly returns None

Python 2 和 Python 3 之间 exec 的语法差异

Python 2 的主要区别之一是 exec 是一个语句,而 eval 是一个内置函数(两者都是 Python 3 中的内置函数)。众所周知,Python 2 中 exec 的官方语法是 exec code [in globals[, locals]]

与大多数 Python 2-to-3 porting guides seem to suggest 不同,CPython 2 中的 exec 语句也可以与 看起来 完全一样的语法一起使用 就像 Python 3 中的 exec 函数调用一样。原因是 Python 0.9.9 有 exec(code, globals, locals) 内置函数!并且该内置函数已替换为 exec 语句 somewhere before Python 1.0 release

由于希望不破坏与 Python 0.9.9 的向后兼容性,Guido van Rossum added a compatibility hack in 1993:如果 code 是长度为 2 或 3 的元组,并且 globalslocals 未传递到 exec 语句否则,code 将被解释为元组的第二个和第三个元素分别是 globalslocals。即使在 Python 1.4 documentation (the earliest available version online) 中也没有提到兼容性黑客;因此许多移植指南和工具的作者并不知道,直到它再次成为 documented in November 2012

第一个表达式也可以是长度为 2 或 3 的元组。在这种情况下,必须省略可选部分。 exec(expr, globals) 形式等价于 globals 中的 exec expr,而 exec(expr, globals, locals) 形式等价于 globals, locals 中的 exec expr。 exec 的元组形式提供了与 Python 3 的兼容性,其中 exec 是一个函数而不是语句。

是的,在 CPython 2.7 中,它被方便地称为向前兼容选项(为什么人们会因为有向后兼容选项而感到困惑),而实际上它已经存在了 20 年的向后兼容性。

因此,虽然 exec 在 Python 1 和 Python 2 中是一个语句,在 Python 3 和 Python 0.9.9 中是一个内置函数,

>>> exec("print(a)", globals(), {'a': 42})
42

在可能每个广泛发布的 Python 版本中都有相同的行为;并在 Jython 2.5.2、PyPy 2.3.1 (Python 2.7.6) 和 IronPython 2.6.1 中工作(对他们密切关注 CPython 的未记录行为表示敬意)。

在 Python 1.0 - 2.7 及其兼容性黑客中,你不能做的是将 exec 的返回值存储到一个变量中:

Python 2.7.11+ (default, Apr 17 2016, 14:00:29) 
[GCC 5.3.1 20160413] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> a = exec('print(42)')
  File "<stdin>", line 1
    a = exec('print(42)')
           ^
SyntaxError: invalid syntax

(这在 Python 3 中也没有用,因为 exec 总是返回 None),或者传递对 exec 的引用:

>>> call_later(exec, 'print(42)', delay=1000)
  File "<stdin>", line 1
    call_later(exec, 'print(42)', delay=1000)
                  ^
SyntaxError: invalid syntax

有人可能实际使用过的模式,尽管不太可能;

或者在列表理解中使用它:

>>> [exec(i) for i in ['print(42)', 'print(foo)']
  File "<stdin>", line 1
    [exec(i) for i in ['print(42)', 'print(foo)']
        ^
SyntaxError: invalid syntax

这是对列表推导的滥用(请改用 for 循环!)。


[i for i in globals().values() if hasattr(i, '__call__')][0] 是语句还是表达式?如果它是一个表达式,为什么我不能将它与 @ 一起用作装饰器?
它是一种表达方式。 42 也是一个表达式,您不能将它与 @ 一起用作装饰器。
装饰器语法是 decorator ::= "@" dotted_name ["(" [parameter_list [","]] ")"] NEWLINE;即你不能使用任意表达式作为装饰器,只能使用一个(可能是虚线的)标识符,后跟可选的调用参数。
没有任何东西可以放在赋值的右侧并且仍然可以编译是表达式。例如,a = b = c 是一个完全有效的语句,它的右侧 b = c 也是如此 - 这不是一个表达式。
值得一提的是 ast.literal_eval,它比 evalexec 更受限制,但使用起来也更安全。我经常使用它来评估可能代表 boolintfloat、...的字符串
q
qrtLs

exec 不是表达式:Python 2.x 中的语句和 Python 3.x 中的函数。它编译并立即评估字符串中包含的语句或语句集。示例: exec('print(5)') # prints 5. # exec 'print 5' 如果你使用 Python 2.x,也不是 exec,print 不是函数 exec('print(5)\nprint(6 )') # 打印 5{newline}6. exec('if True: print(6)') # 打印 6. exec('5') # 什么都不做,什么也不返回。 eval 是一个内置函数(不是语句),它计算表达式并返回表达式产生的值。示例:x = eval('5') # x <- 5 x = eval('%d + 6' % x) # x <- 11 x = eval('abs(%d)' % -100) # x <- 100 x = eval('x = 5') # 无效;赋值不是表达式。 x = eval('如果 1: x = 4') # 无效; if 是一个语句,而不是表达式。 compile 是 exec 和 eval 的低级版本。它不会执行或评估您的语句或表达式,而是返回一个可以执行此操作的代码对象。模式如下: compile(string, '', 'eval') 返回执行 eval(string) 时会执行的代码对象。请注意,您不能在此模式下使用语句;只有一个(单个)表达式是有效的。 compile(string, '', 'exec') 返回执行 exec(string) 时会执行的代码对象。您可以在此处使用任意数量的语句。 compile(string, '', 'single') 类似于 exec 模式,但只需要一个表达式/语句,例如 compile('a=1 if 1 else 3', 'myf', mode='single')


在 Python 3 中,exec() 现在实际上是一个函数。
由于(正如您所指出的),exec 是您所针对的版本中的一个语句,因此包含这些括号具有欺骗性,并且如果您尝试使用 in globals, locals,也会出现错误。
@MikeGraham exec 支持括号和 Python 2 中类似调用的函数
@AnttiHaapala 就分配“支持括号”而言,因为您可以执行 x = (y),这可能是真的。另一个语句转换函数是 print;在 python 2 和 3 中比较 print(1, 2, 3) 的结果。
@habnabit 不是那样的。请在此处阅读 my answer 的底部并感到惊讶。
W
Wu Li

exec 是 for 语句,不返回任何内容。 eval 用于表达式并返回表达式的值。

表达式的意思是“某事”,而语句的意思是“做某事”。


第二段如此简化,几乎成了谎言,如果一个表达式包含一个函数调用,它可以做很多事情。