eval、exec和compile之间有什么区别?

2024-09-19 15:53:16 发布

您现在位置:Python中文网/ 问答频道 /正文

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

谁能解释一下evalexec之间的区别,以及compile()的不同模式是如何适应的


Tags: 函数代码eval模式动态语句execcompile
3条回答

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

表达的意思是“某事”,而陈述的意思是“做某事”

  1. exec不是表达式:Python 2.x中的语句和Python 3.x中的函数。它编译并立即计算字符串中包含的语句或语句集。例如:

     exec('print(5)')           # prints 5.
     # exec 'print 5'     if you use Python 2.x, nor the exec neither the print is a function there
     exec('print(5)\nprint(6)')  # prints 5{newline}6.
     exec('if True: print(6)')  # prints 6.
     exec('5')                 # does nothing and returns nothing.
    
  2. eval是一个内置函数(不是语句),它对表达式求值并返回表达式生成的值。例如:

     x = eval('5')              # x <- 5
     x = eval('%d + 6' % x)     # x <- 11
     x = eval('abs(%d)' % -100) # x <- 100
     x = eval('x = 5')          # INVALID; assignment is not an expression.
     x = eval('if 1: x = 4')    # INVALID; if is a statement, not an expression.
    
  3. compileexeceval的较低级别版本。它不会执行或计算语句或表达式,但会返回一个可以执行该操作的代码对象。模式如下:

  4. compile(string, '', 'eval')返回如果执行了eval(string)将执行的代码对象。请注意,在此模式下,不能使用语句;只有一个(单个)表达式有效

  5. compile(string, '', 'exec')返回如果执行exec(string)将执行的代码对象。您可以在这里使用任意数量的语句

  6. compile(string, '', 'single')类似于exec模式,但只需要一个表达式/语句,例如compile('a=1 if 1 else 3', 'myf', mode='single')

简短的回答,或TL;博士

基本上,^{}用于eval评估单个动态生成的Python表达式,而^{}用于execute动态生成的Python代码,仅用于其副作用

evalexec有以下两个区别:

  1. eval只接受单个表达式exec可以接受包含Python语句的代码块:循环、try: except:class和函数/方法def初始化等等

    Python中的表达式是变量赋值中的值:

    a_variable = (anything you can put within these parentheses is an expression)
    
  2. eval返回给定表达式的值,而exec忽略其代码中的返回值,并始终返回None(在Python 2中,它是一个语句,不能用作表达式,因此它实际上不返回任何内容)

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

在Python3中,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'模式中,它将单个表达式编译成返回该表达式值的字节码

>>> 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时才适用。然后使用^{}在内部将其编译为字节码,这就是差异的真正来源

如果一个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

答案越长,又称血淋淋的细节

execeval

^{}函数(即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
>>> 

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

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

execeval都接受程序/表达式作为包含源代码的strunicodebytes对象运行,或者作为包含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
>>> 

其中,只有exec接受包含语句的源代码,如defforwhileimport、或class、赋值语句(又称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,这是代码看到的全局和局部变量范围。它们默认为globals()locals()在调用execeval的范围内,但是任何字典都可以用于globals,任何mapping都可以用于locals(当然包括dict)。它们不仅可用于限制/修改代码看到的变量,还可用于捕获exec已执行代码创建的变量:

>>> 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)也一直被接受(见下文)

compile

通过预先将源代码编译成code对象,可以使用^{}内置函数加速对execeval相同代码的重复调用。mode参数控制compile函数接受的代码片段类型及其生成的字节码类型。选择是'eval''exec''single'

  • 'eval'模式需要一个表达式,并将生成字节码,运行时将返回该表达式的值

    >>> dis.dis(compile('a + b', '<string>', 'eval'))
      1           0 LOAD_NAME                0 (a)
                  3 LOAD_NAME                1 (b)
                  6 BINARY_ADD
                  7 RETURN_VALUE
    
  • 'exec'接受从单个表达式到整个代码模块的任何类型的python构造,并像执行模块顶级语句一样执行它们。代码对象返回None

    >>> dis.dis(compile('a + b', '<string>', 'exec'))
      1           0 LOAD_NAME                0 (a)
                  3 LOAD_NAME                1 (b)
                  6 BINARY_ADD
                  7 POP_TOP                             <- discard result
                  8 LOAD_CONST               0 (None)   <- load None on stack
                 11 RETURN_VALUE                        <- return top of stack
    
  • 'single''exec'的一种有限形式,它接受包含一个单个语句(或由^{分隔的多个语句)的源代码。如果最后一个语句是一个表达式语句,则生成的字节码也将该表达式的值的repr打印到标准输出(!)

    一个if-elif-else链,一个带有else的循环,以及带有exceptelsefinally块的try被认为是一个语句

    包含2个顶级语句的源片段对于'single'来说是一个错误,除了在Python 2中有一个bug,它有时允许代码中有多个顶级语句;只有第一个被编译;其余的则被忽略:

    在Python 2.7.8中:

    >>> exec(compile('a = 5\na = 6', '<string>', 'single'))
    >>> a
    5
    

    在Python 3.4.2中:

    >>> exec(compile('a = 5\na = 6', '<string>', 'single'))
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 1
        a = 5
            ^
    SyntaxError: multiple statements found while compiling a single statement
    

    这对于制作交互式Python shell非常有用。但是,表达式的值不会返回,即使您eval返回结果代码也是如此

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


除了将源代码编译成字节码之外,compile还支持将abstract syntax trees(Python代码的解析树)编译成code对象;将源代码转换成抽象语法树(用Python编写ast.parse,只调用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'mode转换成一个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

如果我们研究cpython3中的^{}^{}源代码,这是非常明显的;它们都使用相同的参数调用PyEval_EvalCode,唯一的区别是^{} explicitly returns ^{}

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

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

与大多数Python 2-to-3 porting{a15}{a16}{a17}不同,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将被解释为元组的第二和第三个元素分别是globals和^}。甚至在Python 1.4 documentation (the earliest available version online)中也没有提到兼容性黑客;因此,许多关于移植指南和工具的作者并不知道,直到{a21}再次{a22}:

The first expression may also be a tuple of length 2 or 3. In this case, the optional parts must be omitted. The form exec(expr, globals) is equivalent to exec expr in globals, while the form exec(expr, globals, locals) is equivalent to exec expr in globals, locals. The tuple form of exec provides compatibility with Python 3, where exec is a function rather than a statement.

是的,在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未记录的行为,对此表示感谢)

在Pythons 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循环!)

相关问题 更多 >