eval、exec和compile有什么区别?

  • 问题:
  • 我一直在研究Python代码的动态求值,遇到了eval()compile()函数,以及exec语句

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

  • 答案:
  • 基本上,eval用于对单个动态生成的Python表达式求值,并exec用于execute动态生成的Python代码,仅用于其副作用

    evalexec有以下两个区别:

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

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

    a_variable = (anything you can put within these parentheses is an expression)

    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('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来执行具有语句的内容,如果您只是在编译之前将其转换为字节码,而不是将其作为字符串传递:

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

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

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

    >>> 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函数(即Python 2中的语句)用于执行动态创建的语句或程序:

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

    eval函数对a单个表达式返回表达式的值:

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

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

    如果包含源代码的str/unicode/字节被传递给exec,那么它的行为等效于:

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

    eval的行为类似于:

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

    因为在Python中,所有表达式都可以用作语句(在Python中,这些表达式称为Expr节点)abstract grammar如果不需要返回值,则始终可以使用exec。也就是说,您可以使用eval('my\u func(42)exec('my\u func(42),区别在于eval返回my\u 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接受包含语句的源代码,如defforwhile导入、赋值语句(即a.ka=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个附加的位置参数-全局变量局部变量,这是代码看到的全局和局部变量范围。它们默认为调用execeval的作用域内的globals()locals(),但任何字典都可以用于globals,任何映射(当然包括dict)。它们不仅可用于限制/修改代码所看到的变量,而且通常还用于捕获execut代码创建的变量:

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

    (如果显示整个g的值,则时间会长得多,因为execeval会将内置模块作为builtins\自动添加到全局变量中(如果丢失)

    在Python2中,exec语句的官方语法实际上是globals中的exec code,locals,如中所示

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

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

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

    “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构造,并将它们当作模块顶级语句来执行。code对象返回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”的一种有限形式,如果最后一个语句是expression语句,则接受包含单个语句(或由分隔的多个语句)的源代码,生成的字节码还将表达式值的repr打印到标准输出(!)

    一个ifelifelse链,一个包含else的循环,以及带有exceptelsefinally块的if-elif-else链被视为一个语句

    对于'single'来说,包含2个顶级语句的源代码片段是一个错误,但在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

    这对于制作交互式pythonshell非常有用。但是,表达式的值是不返回,即使您对结果代码求值

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

    除了将源代码编译为字节码之外,compile还支持编译abstract syntax trees(Python代码的解析树)转换为code对象;源代码转换为抽象语法树(theast.解析是用Python编写的,只需调用compile(source、filename、mode、PyCF\ONLY_-AST);这些函数用于动态修改源代码,以及动态代码创建,因为在复杂的情况下,将代码作为节点树而不是文本行来处理通常更容易

    虽然eval只允许对包含单个表达式的字符串求值,但可以eval整个语句,甚至可以将编译成字节码的整个模块;也就是说,在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

    使用'exec'模式将其编译为code对象,您可以evalit;函数将返回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

    如果有人调查eval以及execcpython3中的源代码,这一点非常明显;它们都用相同的参数调用PyEval_EvalCode,唯一的区别是https://hg.python.org/cpython/file/ec6ed10d611e/python/bltinmodule.c\l903“rel=”noreferrer“>exec显式返回None

    Python2的一个主要区别是exec是一个语句,eval是一个内置函数(这两个函数都是Python3中的内置函数)。

    与大多数Python 2-3不同porting指南似乎建议,cpython2中的exec语句也可以与python3中的exec函数调用类似的语法使用。原因是Python0.9.9具有exec(code,globals,locals)内置函数!而该内置函数被exec语句在Python1.0发布之前

    因为最好不要破坏与python0.9.9的向后兼容性,Guido van Rossum added a compatibility hack in 1993:如果code是长度为2或3的元组,并且全局变量局部变量没有传递到exec语句中,则代码将被解释为元组的第2和第3个元素分别是全局变量局部变量。甚至在Python1.4文档(最早的在线可用版本);因此许多移植指南和工具的编写者并不知道,直到再次记录2012年11月

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

    是的,在cpython2.7中,它被轻松地称为向前兼容选项(为什么人们会因为有一个向后兼容性选项而迷惑人们呢?),

    因此,虽然exec是Python1和Python2中的一个语句,在Python3和Python0.9.9中是一个内置函数

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

    可能在所有广泛发行的Python版本中都有相同的行为;并且在Jython 2.5.2、pypy2.3.1(Python 2.7.6)和ironpython2.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

    (这在Python3中也没有用处,因为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循环!)。在