在Python中,val、exec和 compile 有什麼區別?

古來聖賢皆寂寞發表於2018-12-15

基本上 eval 上用來評估一個動態生成的 Python 表示式;exec 額外的用於執行動態生成的 python 程式碼。

eval 和 exec 有以下兩個差異

eval 只接受一個表示式,exec 可以接受一個包含了 python 語句的程式碼塊: loops, try: except:, class 以及定義的函式和方法

Python 中的表示式是任何可以作為變數賦值中的值的表示式:

a_variable = (任何你可以放在這個括號內的都是一個表示式)

evale 返回表示式的值,而 exec 忽略返回值的程式碼,並且使用返回 None(在 Python 2 中這是一個宣告,而不是一個表示式,所以沒有任何返回)

在 1.0 - 2.7 版本中,exec 是一個宣告,它總是為函式產生相同型別的程式碼物件,因為需要使用 exec 在函式中產生一個不同的編碼物件。

在 python3 中,exec 上一個函式,它總是產生相同型別的程式碼物件,不管是在函式內部還是模組範圍內呼叫的。

因此,基本上是這樣的

>>> a = 5
>>> eval('37 + a')   # 這是一個表示式
42
>>> exec('37 + a')   # 這時一個表示式宣告,返回值被忽略了(因為返回的是 None )
>>> exec('a = 47')   # 修改一個全域性變數
>>> a
47
>>> eval('a = 47')  # 你無法評估一個表示式
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1
    a = 47
          ^
SyntaxError: invalid syntax

在'exec'模式下編譯,將任意數量的語句編譯成 總是返回None的位元組碼,而在'eval'模式下,它將單個表示式編譯成返回該表示式值的位元組碼。

>>> eval(compile('42', '<string>', 'exec'))  # 程式碼返回 None
>>> eval(compile('42', '<string>', 'eval'))  # 程式碼返回42
42
>>> exec(compile('42', '<string>', 'eval'))  #  程式碼返回 42,但是被 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程式碼的字串傳入進來是,他才會內部進行編譯,生成位元組碼,compile(source, '<string>', 'eval'),這是差異真正來自的地方。

如果一個包含了 python 位元組碼的編碼物件被傳遞給了 exec 和 eval ,他們的行為是一樣的,區別僅僅在於 exce 忽略返回值,仍然返回 None。

所以如果你只想編譯城位元組碼而不是作為字串傳遞的化,可以使用 eval 來執行包含語句的東西。

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

Hello

即使編譯的程式碼包含語句,也可以毫無問題地工作。 它仍然返回None,因為這是從編譯返回的程式碼物件的返回值。

在 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 and eval

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 = 2

>>> my_calculation = '42 * a'

>>> result = eval(my_calculation)

>>> result

84

exec 和 eval 都接受程式/表示式作為包含原始碼的strunicodebytes物件,或者作為包含 Python位元組碼的程式碼物件執行。

如果將包含原始碼的str / unicode / bytes傳遞給exec,則它的行為等同於:

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

和eval類似的表現等同於:

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

由於所有表示式都可以在Python中用作語句(這些語句在Python抽象語法中被稱為Expr節點;反之亦然),如果不需要返回值,則可以始終使用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

>>>

在 Python 2 中,只有exec接受包含語句(如def,for,while,import或class),賦值語句(a.k.a 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

exec 和 eval 都接受2個額外的位置引數 - 全域性變數和區域性變數 - 這是程式碼所看到的全域性和區域性變數範圍。 這些預設為呼叫exec或eval的範圍內的globals()locals(),但任何字典都可以用於全域性變數和任何本地對映(當然包括字典)。 這些不僅可以用來限制/修改程式碼所看到的變數,還可以用於捕獲執行程式碼建立的變數:

>>> g = dict()

>>> l = dict()

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

>>> g['a']

123

>>> l

{'b': 42}

(如果顯示整個g的值,則會更長,因為如果缺少,exec和eval會自動新增內建模組的__builtins__到全域性變數)。

在Python 2中,exec語句的官方語法實際上是globals,locals中的exec程式碼,如

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

然而,exec(code, globals, locals)的替代語法也一直被廣泛接受(見下文)。 compile

內建的 compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1) 可以用來預先將原始碼編譯成程式碼物件,從而加速exec或eval對同一程式碼的重複呼叫。mode引數控制編譯函式接受的程式碼片段的型別以及它產生的位元組碼的型別。 選擇是'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 的迴圈,和一個 try except,else 和 finally 塊被視為是一個單獨的語句。

在 “single”中包含2個頂級語句的原始碼片段是錯誤的,除了在Python 2中有一個錯誤,有時候允許程式碼中有多個頂層語句; 只有第一個是編譯的; 其餘的被忽略:

In Python 2.7.8:

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

And in 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 生成的程式碼,表示式的值也不會返回。

因此exec和eval的最大區別實際上來自編譯函式及其模式。

編譯器除了將原始碼編譯成位元組碼之外,還支援將抽象語法樹(將Python程式碼解析樹)編譯成程式碼物件; 和原始碼到抽象語法樹(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

用'exec'模式compile成程式碼物件,然後你可以eval它; eval函式將返回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中檢視eval和exec原始碼,這是非常明顯的; 它們都使用相同的引數呼叫PyEval_EvalCode,唯一的區別是exec顯式地返回None。

Python 2和Python 3之間exec的語法差異

一個在Python 2的主要差異是,exec 一個宣告和eval是一個內建函式(無論是在Python 3的內建函式)。這是一個眾所周知的事實,在Python 2執行正式的語法是全域性執行程式碼 exec code [in globals[, locals]]

不像大多數Python 2-3移植指南似乎表明,在CPython 2 EXEC語句也可以使用語法,看起來就像在Python 3 exec函式呼叫。原因是,Python 0.9.9有exec(程式碼、全域性變數、區域性變數)內建函式!在Python 1釋出之前,內建的函式被替換了。

因為它是可取的不是Python 0.9.9打破向後相容性,Guido van Rossum說1993的相容性的問題:如果程式碼是一個元組長度為2或3,與全球和當地人不傳遞給exec語句否則,程式碼將被解釋為如果元組的第二元和第三元的全域性變數和當地人的分別。即使在Python 1.4文件(最早可用的線上版本)中也沒有提到相容性破解程式,因此許多移植指南和工具的作者都不知道,直到2012年11月再次被證明:

第一表示式也可以是長度為2或3的元組。在這種情況下,必須省略可選部件。 exec(expr, globals) 相當於執行於全域性,而形式exec(expr,全域性變數、當地人)相當於執行於全域性,當地人。執行器的元組形式提供了與Python 3的相容性,在這裡Python是一個函式而不是一個語句。

是的,在當前的2.7,是輕易被提到的向前相容性選項(為什麼迷惑人,都是一個向後相容性選項),當它實際上已經有二十多年的向後相容性。

因此,執行是Python 1和Python 2的宣告,並在Python 3和Python 0.9.9內建函式,

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

42

是的,在當前的2.7,是輕易被提到的向前相容性選項(為什麼迷惑人,都是一個向後相容性選項),當它實際上已經有二十多年的向後相容性。

因此,執行是Python 1和Python 2的宣告,並在Python 3和Python 0.9.9內建函式,

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:

哪一種模式可能是某人實際使用過的,雖然不太可能;

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

>>> [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

這是列表解析濫用(使用迴圈取反!)。

 

相關文章