4

次のようなコードがあります。

def foo():
    bar = initial_bar = Bar()
    while True:
        next_bar = Bar()
        bar.next_bar = next_bar
        bar = next_bar
    return initial_bar

その意図はBar、リンクされたリストスタイルに従うことができる一連の s が形成されることです。

これはすべて非常にうまくいきました。しかし、いくつかの誤った考えにより、ループの最後の割り当てを 1 行にまとめて、1 行ずつ切り詰めたいと思いました。

def foo():
    bar = initial_bar = Bar()
    while True:
        next_bar = Bar()
        bar = bar.next_bar = next_bar
    return initial_bar

効果的に続くbar = bar.next_bar = next_barように展開されるためです。(そうでないことを除いて。)bar.next_bar = next_barbar = bar.next_bar

問題は、これが機能しないことです。next_bar返された「初期バー」には定義がありません。より明示的な 2 行のソリューションに戻ることで、簡単に回避できますが、何が起こっているのでしょうか?

4

1 に答える 1

5

引き抜く時が来ましたdis

>>> import dis
>>> dis.dis(foo)
  2           0 LOAD_GLOBAL              0 (Bar)
              3 CALL_FUNCTION            0
              6 DUP_TOP             
              7 STORE_FAST               0 (bar)
             10 STORE_FAST               1 (initial_bar)

  3          13 SETUP_LOOP              32 (to 48)
        >>   16 LOAD_GLOBAL              1 (True)
             19 POP_JUMP_IF_FALSE       47

  4          22 LOAD_GLOBAL              0 (Bar)
             25 CALL_FUNCTION            0
             28 STORE_FAST               2 (next_bar)

  5          31 LOAD_FAST                2 (next_bar)
             34 DUP_TOP             
             35 STORE_FAST               0 (bar)
             38 LOAD_FAST                0 (bar)
             41 STORE_ATTR               2 (next_bar)
             44 JUMP_ABSOLUTE           16
        >>   47 POP_BLOCK           

  6     >>   48 LOAD_FAST                1 (initial_bar)
             51 RETURN_VALUE        

これをよく見ると、重要な行 (5 行目、左側の 31 から 47 の位置の数字を参照) で次のようになっていることがわかります。

  • next_bar(31) を 2 回 (34)ロードします。
  • それ (スタック上の最初のコピー) をbar(35) に書き込みます。
  • それ (スタック上の 2 番目のコピー) をbar.next_bar(38, 41) に書き込みます。

これは、最小限のテスト ケースでより明確に見られます。

>>> def a():
...     b = c = d
... 
>>> dis.dis(a)
  2           0 LOAD_GLOBAL              0 (d)
              3 DUP_TOP             
              4 STORE_FAST               0 (b)
              7 STORE_FAST               1 (c)
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE        

それが何をしているのか見てください。これは、b = c = d実際には と同等であることを意味しb = d; c = dます。通常、これは問題になりませんが、最初に言及された場合は問題になります。つまり、クリティカルラインでは、

bar = bar.next_bar = next_bar

と同等ではありません

bar.next_bar = next_bar
bar = next_bar

しかし、むしろ

bar = next_bar
bar.next_bar = next_bar

実際、これは Python ドキュメントのセクション 6.2、単純なステートメント代入ステートメントに記載されています。

割り当てステートメントは式リストを評価し (これは単一の式またはコンマ区切りのリストであり、後者はタプルを生成することに注意してください)、単一の結果オブジェクトを各ターゲット リストに左から右に割り当てます。

そのセクションには、このケースに適用される関連する警告もあります。

警告: 代入の定義は、左側と右側の間のオーバーラップが「安全」であることを暗示していますが (たとえばa, b = b, a、2 つの変数を交換します)、代入先変数のコレクション内でのオーバーラップは安全ではありません! たとえば、次のプログラムは次のように出力します[0, 2]

x = [0, 1]
i = 0
i, x[i] = 1, 2
print x

それを実行することは可能でbar.next_bar = bar = next_barあり、それは当初の望ましい結果をもたらしますが、後でコードを読まなければならない人 (元の作者を含む!) を気の毒に思ってください。ティムがそれらのことを考えていたら、確かに使っていたでしょう。

明示的なものは、紛らわしいコーナーケースよりも優れています。

于 2012-12-19T03:23:39.553 に答える