253

次のコードは、Python 2.5 と 3.0 の両方で期待どおりに機能します。

a, b, c = (1, 2, 3)

print(a, b, c)

def test():
    print(a)
    print(b)
    print(c)    # (A)
    #c+=1       # (B)
test()

ただし、行(B)のコメントを外すと、UnboundLocalError: 'c' not assigned(A)が表示されます。aとの値はb正しく出力されます。これには、次の 2 つの理由から完全に困惑しています。

  1. 行(B)の後のステートメントが原因で、行(A)で実行時エラーがスローされるのはなぜですか?

  2. エラーが発生しているのに、変数ab期待どおりに出力されるのはなぜですか?c

私が思いつく唯一の説明は、ローカル変数cが割り当てによって作成されるということc+=1です。これは、ローカル変数が作成される前であっても、「グローバル」変数よりも優先cされます。もちろん、変数が存在する前にスコープを「盗む」ことは意味がありません。

誰かがこの動作を説明してもらえますか?

4

11 に答える 11

263

Python では、値を関数の内側から割り当てるか外側から割り当てるかによって、関数内の変数の扱いが異なります。変数が関数内で割り当てられている場合、デフォルトではローカル変数として扱われます。したがって、行のコメントを外すと、c値が割り当てられる前にローカル変数を参照しようとしています。

変数が関数の前に割り当てられcたグローバルを参照するようにするには、c = 3

global c

関数の最初の行として。

Python 3に関しては、現在あります

nonlocal c

c変数を持つ最も近い外側の関数スコープを参照するために使用できます。

于 2008-12-16T03:12:56.833 に答える
86

Python は、さまざまなスコープのディクショナリにすべてを保持するという点で少し奇妙です。元の a、b、c は最上位のスコープにあり、最上位の辞書にあります。関数には独自の辞書があります。print(a)andステートメントに到達するとprint(b)、その名前の辞書には何もないため、Python はリストを検索してグローバル ディクショナリでそれらを見つけます。

これc+=1はもちろん と同等c=c+1です。Python がその行をスキャンすると、「ああ、c という名前の変数があります。ローカル スコープの辞書に入れます」と表示されます。次に、割り当ての右側にある c の c の値を探しに行くと、 cという名前のローカル変数が見つかりますが、まだ値がないため、エラーがスローされます。

上記のステートメントは、グローバル スコープの を使用するため、新しいスコープは必要ないglobal cことをパーサーに伝えているだけです。c

行に問題があると言う理由は、コードを生成しようとする前に名前を効果的に探しているためであり、ある意味ではまだその行を実際に実行しているとは考えていません。これは使いやすさのバグだと思いますが、一般的には、コンパイラのメッセージをあまり真剣に受け止めないように学習することをお勧めします。

慰めになるとすれば、この同じ問題を掘り下げて実験するのにおそらく 1 日を費やした後、Guido がすべてを説明した辞書について書いたものを見つけることができました。

更新、コメントを参照してください:

コードを 2 回スキャンすることはありませんが、字句解析と解析の 2 つのフェーズでコードをスキャンします。

このコード行の解析がどのように機能するかを考えてみましょう。lexer はソース テキストを読み取り、文法の「最小コンポーネント」である語彙素に分割します。だからそれがラインに当たったとき

c+=1

それはそれを次のようなものに分割します

SYMBOL(c) OPERATOR(+=) DIGIT(1)

パーサーは最終的にこれを解析木にして実行したいのですが、代入なのでその前にローカル辞書で c という名前を探し、見つからず、辞書に挿入し、マークを付けます初期化されていません。完全にコンパイルされた言語では、シンボルテーブルに入って解析を待つだけですが、2回目のパスの余裕がないため、レクサーは後で作業を楽にするために少し余分な作業を行います. それからのみ、それは OPERATOR を見て、ルールが「演算子 += がある場合、左辺は初期化されているに違いない」と述べていることを確認し、「おっと!」と言います。

ここでのポイントは、実際にはまだ行の解析を開始していないということです。これはすべて、実際の解析の準備として行われているため、行カウンターは次の行に進んでいません。したがって、エラーを通知するとき、それはまだ前の行にあると考えています。

私が言ったように、これはユーザビリティのバグだと主張することができますが、実際にはかなり一般的なことです。一部のコンパイラはそれについてより正直で、「XXX 行またはその周辺でエラーが発生しました」と言っていますが、これはそうではありません。

于 2008-12-16T03:25:29.883 に答える
50

逆アセンブルを見ると、何が起こっているのかが明確になる場合があります。

>>> def f():
...    print a
...    print b
...    a = 1

>>> import dis
>>> dis.dis(f)

  2           0 LOAD_FAST                0 (a)
              3 PRINT_ITEM
              4 PRINT_NEWLINE

  3           5 LOAD_GLOBAL              0 (b)
              8 PRINT_ITEM
              9 PRINT_NEWLINE

  4          10 LOAD_CONST               1 (1)
             13 STORE_FAST               0 (a)
             16 LOAD_CONST               0 (None)
             19 RETURN_VALUE

ご覧のとおり、a にアクセスするためのバイトコードはLOAD_FASTであり、b にはLOAD_GLOBAL. これは、関数内で a が割り当てられていることをコンパイラが識別し、それをローカル変数として分類したためです。ローカルのアクセス メカニズムは、グローバルとは根本的に異なります。つまり、ローカルはフレームの変数テーブルにオフセットが静的に割り当てられます。つまり、グローバルのように高価な dict ルックアップではなく、ルックアップがクイック インデックスになります。このため、Python はprint a「スロット 0 に保持されているローカル変数 'a' の値を取得し、それを出力します」と行を読み取り、この変数がまだ初期化されていないことを検出すると、例外を発生させます。

于 2008-12-16T09:49:28.640 に答える
11

伝統的なグローバル変数のセマンティクスを試すと、Python はかなり興味深い動作をします。詳細は覚えていませんが、「グローバル」スコープで宣言された変数の値を読み取ることはできますが、変更する場合はglobalキーワードを使用する必要があります。これに変更test()してみてください:

def test():
    global c
    print(a)
    print(b)
    print(c)    # (A)
    c+=1        # (B)

また、このエラーが発生する理由は、その関数内で「グローバル」変数と同じ名前の新しい変数を宣言することもでき、完全に別のものになるためです。インタープリターは、呼び出されたこのスコープで新しい変数を作成し、cそれをすべて 1 回の操作で変更しようとしていると見なします。これは、この新しい変数が初期化されていないため、Python では許可されていcません。

于 2008-12-16T03:12:59.690 に答える
5

ここに役立つかもしれない2つのリンクがあります

1:docs.python.org/3.1/faq/programming.html?highlight=nonlocal#why-am-i-getting-an-unboundlocalerror-when-the-variable-has-a-value

2:docs.python.org/3.1/faq/programming.html?highlight=nonlocal#how-do-i-write-a-function-with-output-parameters-call-by-reference

リンク1は、エラーUnboundLocalErrorについて説明しています。リンク2は、テスト関数の書き直しに役立ちます。リンク2に基づいて、元の問題は次のように書き直すことができます。

>>> a, b, c = (1, 2, 3)
>>> print (a, b, c)
(1, 2, 3)
>>> def test (a, b, c):
...     print (a)
...     print (b)
...     print (c)
...     c += 1
...     return a, b, c
...
>>> a, b, c = test (a, b, c)
1
2
3
>>> print (a, b ,c)
(1, 2, 4)
于 2009-11-16T22:12:19.977 に答える
3

これはあなたの質問に対する直接的な回答ではありませんが、拡張割り当てと関数スコープの関係によって引き起こされる別の落とし穴であるため、密接に関連しています。

ほとんどの場合、拡張代入 ( a += b) は単純な代入 ( ) とまったく同じであると考える傾向がありますa = a + b。ただし、これで問題が発生する可能性があります。説明させてください:

Python の単純な代入の仕組みは、 ifaが関数に渡された場合 ( func(a)Python は常に参照渡しであることに注意してください)、渡された をa = a + b変更しないことをa意味します。代わりに、 へのローカル ポインターを変更するだけaです。

ただし、 を使用する場合はa += b、次のように実装されることがあります。

a = a + b

または時々(メソッドが存在する場合)次のように:

a.__iadd__(b)

最初のケース ( がグローバルに宣言されていない限り) では、への代入は単なるポインターの更新であるaため、ローカル スコープ外での副作用はありません。a

2 番目のケースでは、aは実際にそれ自体を変更するため、 へのすべての参照aは変更されたバージョンを指します。これは、次のコードによって示されます。

def copy_on_write(a):
      a = a + a
def inplace_add(a):
      a += a
a = [1]
copy_on_write(a)
print a # [1]
inplace_add(a)
print a # [1, 1]
b = 1
copy_on_write(b)
print b # [1]
inplace_add(b)
print b # 1

そのため、関数の引数に対する拡張代入を避けるのが秘訣です (私はそれをローカル/ループ変数にのみ使用しようとしています)。単純な割り当てを使用すると、あいまいな動作から安全になります。

于 2009-01-24T15:13:11.677 に答える
2

Python インタープリターは、関数を完全な単位として読み取ります。1 回目はクロージャ (ローカル変数) を収集し、2 回目はそれをバイトコードに変換します。

既にお気付きだと思いますが、'=' の左側で使用される名前は暗黙的にローカル変数です。変数へのアクセスを += に変更することで何度も捕まりましたが、突然別の変数になりました。

また、特にグローバルスコープとは何の関係もないことも指摘したいと思います。ネストされた関数でも同じ動作が得られます。

于 2008-12-16T08:58:10.193 に答える
2

c+=1assignscの場合、python は割り当てられた変数がローカルであると想定しますが、この場合はローカルで宣言されていません。

globalまたはnonlocalキーワードを使用します。

nonlocalPython 3 でのみ動作するため、Python 2 を使用していて、変数をグローバルにしたくない場合は、変更可能なオブジェクトを使用できます。

my_variables = { # a mutable object
    'c': 3
}

def test():
    my_variables['c'] +=1

test()
于 2016-11-03T18:52:39.750 に答える