最初の使用後に再割り当てされたときのローカル変数のUnboundLocalError


208

次のコードは、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 assignedat行(A)になります。aおよびの値はb正しく印刷されます。これには、2つの理由で完全に困惑します。

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

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

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

誰かがこの動作を説明できますか?

回答:


215

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

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

global c

関数の最初の行として。

Python 3に関しては、

nonlocal c

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


3
ありがとう。簡単な質問。これは、Pythonがプログラムを実行する前に各変数のスコープを決定することを意味しますか?関数を実行する前に?
tba

7
変数スコープの決定はコンパイラーによって行われます。コンパイラーは通常、プログラムを最初に起動したときに1回実行されます。ただし、プログラムに「eval」または「exec」ステートメントがある場合は、コンパイラーが後で実行される可能性があることにも注意してください。
グレッグヒューギル

2
大丈夫ありがとう。「解釈された言語」は、私が思っていたほどには意味がないと思います。
tba

1
あの「nonlocal」キーワードはまさに私が探していたものでしたが、Pythonにはこれが欠けていたようです。おそらくこれは、このキーワードを使用して変数をインポートするそれぞれの囲みスコープを通じて「カスケード」しますか?
ブレンダン

6
@brainfsck:変数の「検索」と「割り当て」を区別するかどうかを理解するのが最も簡単です。名前が現在のスコープで見つからない場合、ルックアップはより高いスコープにフォールバックします。割り当ては常にローカルスコープで行われます(グローバルまたは非ローカル割り当てを使用globalまたはnonlocal強制する場合を除く)
Steven

71

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

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

上記のステートメントglobal cは、パーサーにcグローバルスコープのを使用することを通知するだけなので、新しいスコープは必要ありません。

実行する行に問題があると言うのは、コードを生成しようとする前に名前を効果的に探しているためであり、ある意味では、まだその行を実際に実行しているとは考えていません。私はそれがユーザビリティのバグだと主張しますが、コンパイラのメッセージをあまり真剣に受け取らないように学ぶことは一般的には良い習慣です。

それが快適であれば、GuidoがExplained Everythingについての辞書について書いたものを見つける前に、おそらくこの同じ問題を掘り下げて実験することに1日費やしたでしょう。

更新、コメントを参照:

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

このコード行の解析がどのように機能するかを検討してください。レクサーは、ソーステキストを読み取り、文法の「最小コンポーネント」である語彙素に分解します。だからそれがラインを打つとき

c+=1

それは何かにそれを分割します

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

パーサーは最終的にこれを解析ツリーにして実行することを望みますが、これは割り当てであるため、実行する前に、ローカル辞書で名前cを探し、それを認識せず、辞書に挿入します。初期化されていない。完全にコンパイルされた言語では、シンボルテーブルに移動して解析を待つだけですが、2回目のパスの贅沢がないため、レクサーは少し余分な作業を行って後で生活を楽にします。だけ、それからそれはOPERATORを見ます、ルールが「あなたが演算子を持っているなら+ =左側は初期化されていなければならない」と言い、そして「おっと!」と言います。

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

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


1
さて、あなたの応答をありがとう。Pythonのスコープについていくつかのことがわかりました。ただし、エラーが行(B)ではなく行(A)で発生する理由はまだわかりません。Pythonはプログラムを実行する前に変数スコープディクショナリを作成しますか?
tba

1
いいえ、それは発現レベルです。私は答えに追加します、私はこれをコメントに入れることができないと思います。
チャーリーマーティン

2
実装の詳細に注意:ではCPythonと、ローカルスコープは通常として扱われていないdict、それは内部だけの配列です(locals()移入されますdict返すように、それへの変更は、新規に作成しないでくださいlocals)。解析フェーズでは、ローカルへの各割り当てを検索し、名前からその配列内の位置に変換し、名前が参照されるときは常にその位置を使用します。関数に入ると、引数以外のローカルUnboundLocalError変数がプレースホルダーに初期化されます。変数が読み込まれ、それに関連付けられたインデックスにまだプレースホルダー値が含まれている場合に発生します。
ShadowRanger 2016年

44

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

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

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


10

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

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

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


お返事ありがとうございます。変数(A)でエラーがスローされる理由が説明されていないと思います。ここでは、変数を出力しようとしているだけです。プログラムは、初期化されていない変数を変更しようとしている行(B)に到達することはありません。
tba

1
Pythonは、プログラムの実行を開始する前に、関数全体を読み取り、解析し、内部バイトコードに変換します。そのため、「ローカル変数にcを変換する」という事実は、値の出力後にテキストで発生するという事実は、問題ではありません。
Vatine

6

明確にする最良の例は次のとおりです。

bar = 42
def foo():
    print bar
    if False:
        bar = 0

を呼び出すとfoo()、これも発生し UnboundLocalErrorますが、line bar=0に到達することはないため、論理的にローカルな変数は作成しないでください。

謎は「Python is the Interpreted Language」にあり、関数の宣言はfoo単一のステートメント(つまり、複合ステートメント)として解釈され、それをばかげて解釈し、ローカルスコープとグローバルスコープを作成するだけです。したがってbar、実行前にローカルスコープで認識されます。

このようなについてはこの投稿をご覧ください。http//blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

この投稿では、変数のPythonスコーピングの完全な説明と分析を提供します。


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)

4

これは質問への直接の回答ではありませんが、拡張割り当てと関数スコープの関係によって引き起こされるもう1つの問題であるため、密接に関連しています。

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

Pythonの単純な割り当ての仕組みはa、が関数に渡された場合(のようにfunc(a)、Pythonは常に参照渡しであることに注意)、渡されa = a + baを変更しないことを意味します。代わりに、へのローカルポインタを変更するだけ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

したがって、トリックは、関数の引数への拡張割り当てを回避することです(私はローカル/ループ変数にのみ使用するようにしています)。単純な割り当てを使用すると、あいまいな動作から安全になります。


2

Pythonインタープリターは、関数を完全なユニットとして読み取ります。私はそれを2つのパスで読み取ると考えます。1回はそのクロージャ(ローカル変数)を収集し、次にもう一度バイトコードに変換します。

ご存知のとおり、 '='の左側で使用されている名前はすべて暗黙的にローカル変数です。変数アクセスを+ =に変更することに気づかされて、突然別の変数になりました。

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


2

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

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

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

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

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

test()

1

クラス変数に到達する最良の方法は、クラス名で直接アクセスすることです

class Employee:
    counter=0

    def __init__(self):
        Employee.counter+=1

0

Pythonでは、ローカル、クラス変数、グローバル変数のすべてのタイプの変数に対して同様の宣言があります。メソッドからグローバル変数を参照すると、Pythonは実際にはまだ定義されていないメソッド自体から変数を参照していると見なし、エラーをスローします。グローバル変数を参照するには、globals()['variableName']を使用する必要があります。

あなたのケースでは、それぞれa、b、cの代わりにglobals()['a]、globals()[' b ']、globals()[' c ']を使用してください。


0

同じ問題が気になります。を使用nonlocalglobalて問題を解決できます。
ただし、の使用には注意が必要ですnonlocal。ネストされた関数に対して機能します。ただし、モジュールレベルでは機能しません。こちらの例をご覧ください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.