print or plan and not print()()() 的疑問?

這個表達式為什麼不是先算 not print()()()?

布爾運算符的順序是 先not,然後and,最後or。

所以我認為 print or "plan" and not print()()()的運算順序是:

先算括弧"plan" and not print()()().又因為括弧里有not ,而not 優先於and ,所以還是先算not print()()(),這樣就會產生一個TypeError。

事實上,python不會執行『plan" ans not print()()() 。為什麼呢?

不知道我在那個環節出了錯。


如果有這個疑問,大體上可能Python是你第一次學習入了門的編程語言。

幾乎所有的高級語言里,對於二目邏輯運算(與、或,但這裡不包括位運算)都有一個短路運算特性,即前運算數已經可以確定結果時,後續內容不進行計算(『短路』了)。

話說你題目標題里的情況和題目中的語句不一致啊……不過這裡也給你一個通用一點的解析方法,以後不用去招人問這些個玩意了。如果你是個CS專業的大學生,那麼編譯原理課上學過AST的話其實就能省事很多,但即使如此,我們也先簡單看看,如何在Python里看看語句是怎麼一步步轉成可以執行的玩意的。注意,不要直接一開始就歪到位元組碼去了……那才是真繞遠路。

這裡介紹一個內置模塊,ast。它的作用其實就是個Python的parser,專門轉換成抽象語法樹的。幾行代碼

import ast
code = "print or ("plan" and not print()()())"
print(ast.dump(ast.parse(code)))

你會得到一個這樣的結果

Module(body=[Expr(value=BoolOp(op=Or(), values=[Name(id="print", ctx=Load()), BoolOp(op=And(), values=[Str(s="plan"), UnaryOp(op=Not(), operand=Call(func=Call(func=Call(func=Name(id="print", ctx=Load()), args=[], keywords=[]), args=[], keywords=[]), args=[], keywords=[]))])]))])

可以看到,這裡的and和or都是BoolOp(帶短路),而Not是單目運算,所以是個UnaryOp(沒有多個運算數/運算數不是個列表的運算符),函數調用的括弧是Call,plan這個字面量則直接表示成了Str,而print則是在上下文中讀取print這個名字綁定的內容。這裡構建了一個抽象語法樹,有耐心你可以自己排個版,你的代碼里的所有操作都能按照這個結構描述。上述玩意大體上表示的樹可以這麼畫出來

AST表示了各元素中的邏輯關係,大體是指這樣:假設不發生短路,則對某個節點的操作(求值)必須要求其孩子都已經求值。這個玩意怎麼求值的呢?它通過深度優先遍歷走一遭(可以看成一個遞歸),按照先序遍歷來求值(於是演算法與數據結構課程上來了)。

大體上是這樣的感覺,(這裡只是個示意,實際上並不是這麼寫的,實際情況中,這裡還有更複雜的情況,以及結果不通過AST直接計算,而是產生bytecode再計算)

def parse(ast_node):
if ast_node.body是個字面量:
return ast_node.body
elif ast_node.body是個Op:
if ast_node.body是個UnaryOp:
child_val = parse(ast.child)
return ast_node.body.calc(child_val)
else:
child_val = []
for child in ast_node.child:
child_val.append(parse(child))
if ast_node.body是個BoolOp
if 可以短路:
break
return ast_node.body.calc(child_val)
……

首先算左邊那個print,得到結果是一個function reference,是從上下文中載入的print這個name(默認就是__buildin__.print)。這個值是個非0、非False、非None、非[]的玩意,然後交給邏輯算符or;or算符得到左目非假,此時結果必為真,因此觸發短路,右邊的東西都不算,於是or的右目的語法樹部分壓根就沒進行parse,自然也就完全不會進行計算了。

這裡整個流程包含的知識點包括編譯原理的部分內容,也包括數據結構的樹相關的內容,同時邏輯算符的短路運算也屬於幾乎所有語言的常見通用特性。當然,現在不懂沒關係,記住這裡有些知識點就好了,如果有機會學到這些東西,請認真學。

回到最初的問題:運算優先順序到底是怎麼來的。

人看到括弧就會先算括弧,但通常情況下對於parser而言,並不是跳著先算括弧。括弧的作用是在語言元素區分的時候用的,即生成AST的時候用的,用於構建計算的依賴關係。對於計算中的括弧而言(不包括函數調用的括弧),括弧內的東西被當作了一個整體元素。

例如,如果你寫的語句反倒是這個樣子

(print or "plan") and not print()()()

那麼AST則會變成這樣:

最後還是補充一點,奇怪的寫法並不值得鼓勵,例如這裡這些寫法,連著三個函數調用的括弧,意味著如果不滿足短路條件,就一定會拋異常(按照AST看,會引用print後求值後,因為print()返回一個None而無法進行下一次求值,拋出None can not call?)。

邏輯短路的特性,更多是用於一些取值檢查,或者用於在做一些演算法的時候用來清晰化邏輯。現代化的高級語言,寫出來的東西首先是給人看的,然後才是給機器看的,一些所謂的小技巧遠不如代碼可讀性有價值。

——————————————————————————

看到有人把短路計算說成惰性求值,為了減少誤解,這裡補充一下關於惰性求值到底是個啥的問題。

惰性求值,意義上是當真正要用某個東西的時候才求值。

其實惰性求值要真正有用,至少要求求值過程中是沒有副作用的,否則不可確定的求值順序對於命令式語言而言會帶來比較多的災難,因為很有可能完全無法把握求值順序,而被副作用影響的變數值不可確定。對於不特別嚴格的多範式語言,惰性求值往往是個需要有特別標註才能實現的操作。例如Python里,惰性求值可以利用lambda和functional包里的一些工具來模擬。

但是這麼一來,嚴格函數式語言里惰性求值就有意義了。例如haskell里,寫個斐波那契數列

fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

同樣是個遞歸寫法,卻壓根沒有邊界條件,沒說算到多少位是個頭,但是卻不會在計算的時候直接爆棧,就是因為惰性求值的緣故:只有當你取到第100個元素的時候,它才會算到第100個,而並不會算之後的東西。私以為惰性求值對於函數式語言而言,是為了讓語言去描述需要計算的數學和邏輯關係,而略去計算細節(如邊界條件、為內存保留的最大範圍等)的重要因素。

——————————————

順帶一提,技術問題不要去相信百度百科,不然容易掉溝里了還不自知。

百度百科裡稱自己引用了wikipedia的內容,但是實際上wikipedia上怎麼寫的,可以自己看

Lazy evaluation - Wikipediaen.wikipedia.org

Short-circuit evaluationen.wikipedia.org

https://zh.wikipedia.org/wiki/%E6%83%B0%E6%80%A7%E6%B1%82%E5%80%BCzh.wikipedia.org

https://zh.wikipedia.org/wiki/%E7%9F%AD%E8%B7%AF%E6%B1%82%E5%80%BCzh.wikipedia.org

同作為Evaluation strategies,這兩個東西就是平級的,說實話我也算看了不少材料的人了……真沒見過把Short-circuit evaluation算作lazy evaluation中的一部分的任何一份比較權威的材料。怎麼看都是全世界只有百度百科這麼寫了……


感謝 C 大 @Coldwings 的詳細解答= =

我在評論區里也犯錯了Orz

題中的這種特性是短路運算,,並不能將其歸為惰性計算

真誠反思,反悔中

再次說聲抱歉= =


涉及 or 的一個特性

or 從左到右分割為兩個表達式

關於 or 官方有一個說法

This is a short-circuit operator, so it only evaluates the second argument if the first one is false.

即 or 左側值為 True 的時候,不計算右邊的值

回到你的問題本身

print or "plan" and not print()()()

or 的左右兩側被分割為兩個部分,一部分是 print ,一部分是 "plan" and not print()()()

print 是一個 built-in 的變數,不為 None ,所以最後在參與邏輯運算的時候等價於 True

所以你右側的值不會參與計算

如果你把前面的 print 換成 False ,那麼右側的值就會參與運算。


@黃哥

另外,dis 操作是反彙編操作,,但是出來的東西有個官方稱呼叫 bytecode 位元組碼(

Python 官方的描述

The dis module supports the analysis of CPython bytecode by disassembling

還是要嚴謹一點,誤導初學者可不好啊,逃


這裡Python有點惰性求值的意思

既「用不到的結果不用算」,cpython在這裡這樣實現應該是處於摳效率的打算

另外。。。

@黃哥 編譯後的結果不叫彙編,叫PythonByteCode(Python位元組碼)

你見過哪個彙編長這樣的?

不要誤導新人/爆筋

更新


dis

Disassembler for Python bytecode

將你的問題用類似的問題描述一下

1 or 2 and not 3 / 0

請看下面,反彙編,反編譯成Python位元組碼。

24 BINARY_DIVIDE 二元除法 後面沒有值

25 UNARY_NOT 一元NOT 後面沒有值

可讀性差的表達式,有歧義的時候,加括弧吧。


這個問題,我當時學習的時候叫「短路求值」,依稀記得是編譯原理裡面的中間代碼生成那部分的。

關於短路求值,維基百科看一下就明白:

維基百科-短路求值zh.m.wikipedia.org

這種特性其實有時候能幫我們免去一些麻煩。

比如說能通過一個邏輯或來給變數賦默認值:

邏輯或

邏輯與可以用來避免錯誤:

邏輯與

有異常還是不要依賴邏輯運算,這裡例子只是說明短路求值的優化效果。


運算執行不是優先,而是順序執行(邏輯運算就是判斷跳轉,所以才有什麼「短路說法」),只是語法結合規則,就是符合人語言表達描述的邏輯


推薦閱讀:

如何在mac版本的python里安裝pip?
新學python,編寫helloword.py提示無效語法。?
Youtube上有哪些好的python視頻教程?
python自學菜鳥 expected an indented block什麼意思?
這段python代碼如何理解?解釋器是如何實現這種結果的?

TAG:Python | Python入門 |