簡體   English   中英

python中增廣運算符(定界符)的求值順序

[英]Evaluation order of augmented operators (delimiters) in python

如果我在 python 中評估以下最小示例

a = [1, 2, 3]
a[-1] += a.pop()

我明白了

[1, 6]

所以似乎這被評估為

a[-1] = a[-1] + a.pop()

其中每個表達式/操作數將按順序計算

third = first + second

所以在左側 a[-1] 是第二個元素,而在右側是第三個。

a[1] = a[2] + a.pop()

有人可以向我解釋如何從文檔中推斷出這一點嗎? 顯然 '+=' 在詞法上是一個也執行操作的定界符(參見此處)。 這對其評估順序意味着什么?

編輯:

我試圖在評論中澄清我的問題。 我將其包含在此處以供參考。

我想了解在詞法分析期間是否必須以特殊方式(即通過擴展它們)處理增強運算符,因為您必須復制一個表達式並對其進行兩次評估。 這在文檔中並不清楚,我想知道在哪里指定了這種行為。 其他詞法定界符(例如'}')表現不同。

讓我從你最后問的問題開始:

我想了解在詞法分析期間是否必須以特殊方式(即通過擴展它們)處理增強運算符,

那個很簡單; 答案是不”。 標記只是一個標記,詞法分析器只是將輸入划分為標記。 就詞法分析器而言, +=只是一個標記,這就是它為它返回的內容。

順便說一句,Python 文檔區分了“運算符”和“標點符號”,但這對於當前的詞法分析器來說並不是真正的顯着差異。 在基於運算符優先級解析的解析器的某些先前化身中,這可能是有意義的,其中“運算符”是具有相關優先級和關聯性的詞位。 但我不知道 Python 是否曾經使用過那個特定的解析算法; 在當前的解析器中,“運算符”和“標點符號”都是在語法規則中出現的字面詞位。 如您所料,詞法分析器更關心標記的長度( <=+=都是兩個字符的標記),而不是解析器中的最終使用。

“脫糖”——將某種語言結構轉換為更簡單結構的源轉換的技術術語——通常不在詞法分析器或解析器中執行,盡管編譯器的內部工作不受行為准則的約束。 一種語言是否有脫糖組件通常被認為是一個實現細節,並且可能不是特別明顯; Python 確實如此。 Python 也不向其標記器公開接口。 tokenizer模塊是純 Python 中的重新實現,它不會產生完全相同的行為(盡管它足夠接近成為有用的探索工具)。 但是解析器暴露在ast模塊中,它提供了對 Python 自己的解析器的直接訪問(至少在 CPython 實現中),並且讓我們看到在構造 AST 之前沒有進行脫糖(注意:需要Python3.9 用於indent選項):

>>> import ast
>>> def showast(code):
...    print(ast.dump(ast.parse(code), indent=2))
...
>>> showast('a[-1] += a.pop()')
Module(
  body=[
    AugAssign(
      target=Subscript(
        value=Name(id='a', ctx=Load()),
        slice=UnaryOp(
          op=USub(),
          operand=Constant(value=1)),
        ctx=Store()),
      op=Add(),
      value=Call(
        func=Attribute(
          value=Name(id='a', ctx=Load()),
          attr='pop',
          ctx=Load()),
        args=[],
        keywords=[]))],
  type_ignores=[])

這會產生您期望從語法中得到的語法樹,其中“增強賦值”語句表示為assignment中的特定產生式:

assignment:
    | single_target augassign ~ (yield_expr | star_expressions)

single_target是一個單一的可賦值表達式(例如一個變量,或者,在這種情況下,一個下標數組); augassign是擴充賦值運算符之一,其余的是賦值右側的替代操作。 您可以忽略“柵欄”語法運算符~ 。) ast.dump生成的解析樹非常接近語法,並且根本沒有脫糖:

                      
         --------------------------
         |         |              |
     Subscript    Add            Call
     ---------           -----------------
      |     |            |        |      |
      a    -1        Attribute   [ ]    [ ]
                      ---------
                       |     |
                       a   'pop'

奇跡發生在之后,我們也可以看到,因為 Python 標准庫還包含一個反匯編程序:

>>> import dis
>>> dis.dis(compile('a[-1] += a.pop()', '--', 'exec'))
  1           0 LOAD_NAME                0 (a)
              2 LOAD_CONST               0 (-1)
              4 DUP_TOP_TWO
              6 BINARY_SUBSCR
              8 LOAD_NAME                0 (a)
             10 LOAD_METHOD              1 (pop)
             12 CALL_METHOD              0
             14 INPLACE_ADD
             16 ROT_THREE
             18 STORE_SUBSCR
             20 LOAD_CONST               1 (None)
             22 RETURN_VALUE

可以看出,試圖將增強賦值的評估順序總結為“從左到右”只是一個近似值。 如上面的虛擬機代碼所示,這是實際發生的情況:

  1. “計算”目標聚合及其索引(第 0 行和第 2 行),然后復制這兩個值(第 4 行)。 (重復意味着目標及其下標都不會被計算兩次。)

  2. 然后使用重復的值來查找元素的值(第 6 行)。 因此,此時評估a[-1]的值。

  3. 然后計算右側表達式( a.pop() )(第 8 行到第 12 行)。

  4. 這兩個值(在這種情況下都是 3)與INPLACE_ADD組合,因為這是一個ADD增強分配。 在整數的情況下, INPLACE_ADDADD之間沒有區別,因為整數是不可變的值。 但是編譯器不知道第一個操作數是整數。 a[-1]可以是任何東西,包括另一個列表。 因此,它會發出一個操作數,該操作數將觸發使用__iadd__方法而不是__add__ ,以防有區別。

  5. 原始目標和下標,從第 1 步開始就一直在堆棧上耐心等待,然后用於執行下標存儲(第 16 和 18 行。下標仍然是在第 2 行-1處計算的下標。但此時a[-1]指的是a的不同元素。需要旋轉才能將參數 for 轉換為正確的順序。因為賦值的正常評估順序是首先評估右側,虛擬機假設新值將位於堆棧的底部,然后是對象及其下標。

  6. 最后, None作為語句的值返回。

Python 參考手冊中記錄了賦值語句和增強賦值語句的精確工作原理。 另一個重要的信息來源是__iadd__特殊方法的描述 增強賦值操作的評估(和評估順序)非常令人困惑,以至於有一個專門針對它的編程常見問題解答,如果您想了解確切的機制,值得仔細閱讀。

盡管這些信息很有趣,但值得補充的是,編寫依賴於增強賦值中評估順序細節的程序不利於生成可讀代碼。 在幾乎所有情況下,都應避免依賴於過程的非顯而易見細節的增強分配,包括諸如作為該問題目標的陳述。

rici 很好地展示了 CPython 參考解釋器的幕后情況,但語言規范中有一個更簡單的“事實來源”,它保證任何Python 解釋器(不僅僅是 CPython,還有 PyPy、Jython 、IronPython、Cython 等)。 在語言規范中,在第 6 章:表達式,第 6.16 節,評估順序下,它指定:

Python 從左到右計算表達式。 請注意,在評估分配時,右側先於左側評估。

第二句話聽起來像是一般規則的例外,但事實並非如此。 =的賦值(包括帶+=之類的擴展賦值)不是 Python 中的表達式( 3.8 中引入的 walrus 運算符一個表達式,但它只能分配給裸名,因此永遠沒有任何東西可以“評估”左側,它純粹存儲在那里,從不讀取),它是一個語句,並且賦值語句有自己的評估順序規則 這些分配規則指定:

賦值語句計算表達式列表(請記住,這可以是單個表達式或逗號分隔的列表,后者產生一個元組)並將單個結果對象從左到右分配給每個目標列表。

這證實了表達評估令文件中的第二句話; 首先評估表達式列表(要分配的事物),然后從那里開始對目標的分配。 因此,根據語言規范本身, a[-1] += a.pop()必須首先完全評估a.pop() (“表達式列表”),然后執行賦值。

這種行為是語言規范所要求的,並且已經有一段時間了,因此無論您使用什么 Python 解釋器都可以依賴它。

也就是說,我建議不要使用依賴於 Python 的這些保證的代碼 一方面,當您切換到其他語言時,規則會有所不同(在某些情況下,例如 C 和 C++ 中的許多類似情況,因標准版本而異,沒有“規則”,並試圖在表達式的多個部分會產生未定義的行為),因此越來越依賴 Python 的行為會妨礙您使用其他語言的能力。 除此之外,它仍然會令人困惑,只需稍作更改即可避免混淆,例如,在您的情況下,更改:

a[-1] += a.pop()

只是:

x = a.pop()
a[-1] += x

其中,雖然不可否認是兩條線,因此劣勢!!! , 實現了相同的結果,但開銷沒有意義,而且更清晰。

TL;DR:Python 語言規范保證+=的右側在擴展賦值操作開始之前被完全評估,並且左側的任何代碼都被評估。 但是為了代碼清晰,任何依賴於該保證的代碼都應該被重構以避免上述依賴。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM