Skip to content

Python Referenced Before Assignment In Enclosing Scope Definition

直接上代码

def foo(): a = 1 def bar(): a = a + 1 return bar()


运行foo(),报错


import dis def foo(): a = 1 def bar(): a = a + 1 dis.dis(bar) return bar()

dis打印的内容如下:



很容易明白其意思,,应该是从某个地方加载a,1,执行加法,写回a;

在CPython源码中,搜索“referenced before assignment”


#define NAME_ERROR_MSG \ "name '%.200s' is not defined" #define GLOBAL_NAME_ERROR_MSG \ "global name '%.200s' is not defined" #define UNBOUNDLOCAL_ERROR_MSG \ "local variable '%.200s' referenced before assignment" #define UNBOUNDFREE_ERROR_MSG \ "free variable '%.200s' referenced before assignment" \ " in enclosing scope"

接着搜索UNBOUNDLOCAL_ERROR_MSG

TARGET(LOAD_FAST) x = GETLOCAL(oparg); if (x != NULL) { Py_INCREF(x); PUSH(x); FAST_DISPATCH(); } format_exc_check_arg(PyExc_UnboundLocalError, UNBOUNDLOCAL_ERROR_MSG, PyTuple_GetItem(co->co_varnames, oparg)); break;TARGET(DELETE_FAST)     x = GETLOCAL(oparg);     if (x != NULL) {         SETLOCAL(oparg, NULL);         DISPATCH();     }     format_exc_check_arg(         PyExc_UnboundLocalError,         UNBOUNDLOCAL_ERROR_MSG,         PyTuple_GetItem(co->co_varnames, oparg)         );     break; static void format_exc_unbound(PyCodeObject *co, int oparg) {     PyObject *name;     /* Don't stomp existing exception */     if (PyErr_Occurred())         return;     if (oparg < PyTuple_GET_SIZE(co->co_cellvars)) {         name = PyTuple_GET_ITEM(co->co_cellvars,                                 oparg);         format_exc_check_arg(             PyExc_UnboundLocalError,             UNBOUNDLOCAL_ERROR_MSG,             name);     } else {         name = PyTuple_GET_ITEM(co->co_freevars, oparg -                                 PyTuple_GET_SIZE(co->co_cellvars));         format_exc_check_arg(PyExc_NameError,                              UNBOUNDFREE_ERROR_MSG, name);     } }

初步判断是在执行LOAD_FAST时,报出的错误;

如果能够调试就好了,以后试试吧;

顺便提一下,CpythonDoc目录下有这样的解释



   >>> x = 10
   >>> def foo():
   ...     print(x)
   ...     x += 1


results in an UnboundLocalError:


   >>> foo()
   Traceback (most recent call last):
     ...
   UnboundLocalError: local variable 'x' referenced before assignment


This is because when you make an assignment to a variable in a scope, that
variable becomes local to that scope and shadows any similarly named variable
in the outer scope.  Since the last statement in foo assigns a new value to
``x``, the compiler recognizes it as a local variable.  Consequently when the
earlier ``print(x)`` attempts to print the uninitialized local variable and
an error results.

お久しぶりです。
最近はPython人気ですね。新規参入者の方も増えてきたので、スコープについて知っている限り書いていこうと思います。
レベルは初級者+ ~ 中級者といったところかな。わからないところは読み飛ばすとよいぞ。
関数とか変数がわからない方には厳しいので一旦別のチュートリアルとかを読んでくることをオススメします。

この記事では諸事情によりPython3.5.1と3.6.1を使って動作検証してます。多分問題ないけど、間違ってたらバージョンのせいにします(`・ω・´)

スコープとは

スコープとは変数の有効な範囲です。

Pythonのスコープは 4つにわけられ、その頭文字をとってLEGB と言われています。
それぞれ「Local scope」「Enclosing (function’s) scope」「Global Scope」「Built-in scope」です。
イメージ的には、左に行くほど狭く強く、右に行くほど広く弱いです。強いとか弱いとかは何かというとは優先度なわけですが、これは後述します。

まずはそれぞれのスコープの特性を説明します。

Local(ローカル)スコープ

ローカルスコープが指す範囲は関数の中です。
変数がローカルスコープに属する条件は「関数内で定義された」場合のみです。
定義と一口に言っても記述方法はいくつかあります。言葉だけでは伝わらない思いがあるので実際に動かしてみましょう。

たとえば、こんな謎の関数があるとします。

def something(a, b=1, *c, **d): e =2def f(): pass# 空の関数定義class g: pass# 空のクラス定義importosas h # モジュールのインポートprint(locals())# locals はローカルスコープの変数を取得する関数

この関数の実行結果は以下のような感じになるんじゃないでしょうか。
※辞書の順番や、モジュールパス、アドレスなどは環境毎に異なります。

>>> something(0)# 見やすいように整形したけど普通はこんなふうに表示されないので注意{'a': 0,'b': 1,'c': (),'d': {},'e': 2,'f': <function something.<locals>.f at 0x107c52048>,'g': <class'__main__.something.<locals>.g'>,'h': <module 'os'from'/usr/local/lib/python3.5/os.py'>,}

a, b, c, d は関数の引数として定義された変数です。どんな引数であっても属するスコープは同じです。
e は代入によって定義された変数ですね。
f は関数の定義です。g はクラスの定義です。意外ですか?関数やクラスの定義もその例外ではありません。
h はインポートしたモジュールです。これも定義なのです。(from import であっても同じです)
上記はすべてローカルスコープに属する変数、つまりローカル変数です。

関数外からアクセスするともれなく NameError になるはずです。ならなければ「すでに定義されている」ことを疑ってみてください。

>>> b Traceback (most recent call last): File "<stdin>", line 1,in<module>NameError: name 'b'isnot defined

Global(グローバル)スコープ

さて、LEGBの順番で行けば次はエンクロージングスコープのはずですが、あいつは少し難しいので最後にします。

他の言語を経験しているとどこからでも参照できる変数が属するスコープだろうと考えてしまいそうですが、実はPythonのグローバルスコープはそんなに広くありません。
どこまでかといえば、モジュール(ファイル)です。他のモジュールから変数を参照したい場合はインポートが必要になります。

同じモジュール(ファイル)のグローバルスコープに書かれた変数をグローバル変数と呼ぶのに対し、別モジュールに書かれたグローバル変数をモジュール変数と呼び、モジュールオブジェクトの属性として参照できます。
モジュール経由で参照できる変数はグローバル領域に書かれたものだけというわけですね。よく考えれば当たり前のことでしたね。

たとえばこういうモジュールがあったとすれば、グローバルスコープに属するのは b, c です。

b =1   def c(d): e =2

このモジュールに a.py という名前をつけ、インポートしてみましょう。

>>>import a >>>dir(a)# dir は対象オブジェクトの属性を表示する関数(__で囲まれてるのは特殊な属性なので無視して良い)['__builtins__','__cached__','__doc__','__file__','__loader__','__name__','__package__','__spec__','b','c']>>> a.b1>>> a.c(2)>>>globals()# globals()を使うことでグローバル変数一覧を取得できる。{'__package__': None,'__loader__': <class'_frozen_importlib.BuiltinImporter'>,'__builtins__': <module 'builtins'(built-in)>,'__doc__': None,'__spec__': None,'__name__': '__main__','a': <module 'a'from'a.py'>,}

aモジュールのモジュール変数として b, c が登録されているのがわかりますね。
一回定義したらどこからでも参照できる、そんな便利な変数はPythonにはないのでした。定義したら逐一インポートしてあげましょう。

この辺がちゃんと理解できているとユニットテストで処理をパッチするということがどういうことか理解しやすくなります。その辺の記事はこちら。

ちなみに先程から使っている対話モード(>>> で始まってるコード)に書いたコードはグローバルスコープに属していると思ってください。

Builtin(ビルトイン)スコープ

ビルトインスコープに属する変数はどこからでも参照できます。
いやいや、上でどこからも参照できる変数は定義できないって言ったじゃんということになるのですが、ビルトインスコープに対して変数を追加定義することは基本的にできません。
このスコープに属する変数は定義されていなくとも、どこからでも使うことができます。

変数と言いましたが、その多くは関数(とクラス)です。これらの関数はビルトイン関数とか組み込み関数と呼ばれます。予約語とは違うので混同しないように注意しましょう。(def とか class とかの構文のキーワードが予約語です)
int, str, len, rangeなどのよく使う関数から先程使ったlocals, globals も全部ビルトイン関数(一部はクラスでもあります)です。

(ここは余裕がある方だけ読んでくれればOK)先程ビルトインスコープに変数を追加することは「基本的」にできないと言いましたが実はできます。
ビルトインスコープはモジュールとして存在しています。その実体は builtins モジュール です。
この builtins モジュールに属性を追加するとどこからでも参照できちゃうのです。

>>>import builtins # python3.? からインポートできるよ!>>>test# まだ定義されてないから参照できない Traceback (most recent call last): File "<stdin>", line 1,in<module>NameError: name 'test'isnot defined >>>dir(builtins)['ArithmeticError','AssertionError','AttributeError','BaseException','BlockingIOError','BrokenPipeError','BufferError','BytesWarning','ChildProcessError','ConnectionAbortedError','ConnectionError','ConnectionRefusedError','ConnectionResetError','DeprecationWarning','EOFError','Ellipsis','EnvironmentError','Exception','False','FileExistsError','FileNotFoundError','FloatingPointError','FutureWarning','GeneratorExit','IOError','ImportError','ImportWarning','IndentationError','IndexError','InterruptedError','IsADirectoryError','KeyError','KeyboardInterrupt','LookupError','MemoryError','NameError','None','NotADirectoryError','NotImplemented','NotImplementedError','OSError','OverflowError','PendingDeprecationWarning','PermissionError','ProcessLookupError','RecursionError','ReferenceError','ResourceWarning','RuntimeError','RuntimeWarning','StopAsyncIteration','StopIteration','SyntaxError','SyntaxWarning','SystemError','SystemExit','TabError','TimeoutError','True','TypeError','UnboundLocalError','UnicodeDecodeError','UnicodeEncodeError','UnicodeError','UnicodeTranslateError','UnicodeWarning','UserWarning','ValueError','Warning','ZeroDivisionError','__build_class__','__debug__','__doc__','__import__','__loader__','__name__','__package__','__spec__','abs','all','any','ascii','bin','bool','bytearray','bytes','callable','chr','classmethod','compile','complex','copyright','credits','delattr','dict','dir','divmod','enumerate','eval','exec','exit','filter','float','format','frozenset','getattr','globals','hasattr','hash','help','hex','id','input','int','isinstance','issubclass','iter','len','license','list','locals','map','max','memoryview','min','next','object','oct','open','ord','pow','print','property','quit','range','repr','reversed','round','set','setattr','slice','sorted','staticmethod','str','sum','super','tuple','type','vars','zip']>>> builtins.test=123>>>test# ビルトインスコープに登録すると参照できた123>>> builtins is __builtins__ # 実はグローバルスコープに __builtins__ という名前で置かれてるTrue

できるとはいえ、これは裏技的な感じなのでよいこの皆さんはやめましょう。

Enclosing (エンクロージング)スコープ

「Enclosing function’s scope」でもOKです。「function’s」はなくてもよさそうです。

出鼻をくじくようですが、前述の通りこのスコープが一番ややこしいです。正直わからなければ飛ばしてしまってもいいと思いますん。

端的にいうと関数の外側のローカルスコープです。
関数の外側は関数じゃねーだろって?いやいや、実は関数の場合もあるんですよ。
関数の中で関数は定義できますし、クラスだって定義できます。このスコープは関数の中で関数が定義されている、そんな場合に初めて意識するスコープということです。

こんな感じにキモくネストしている関数aがあるとします。

g =1   def a(b): c =3def aa(bb): def aaa(bbb): ccc =333print('this is aaa',locals())print('c:', c,'cc:', cc,'g:', g,'gg:', gg) cc =33print('this is aa',locals()) aaa(222)print('this is a',locals()) aa(22)   gg =11

関数の最深部、aaa関数では外側の関数内で定義された変数c,cc をそれぞれ参照しています。
これを実行するとこんな感じになります。

>>> a(2) this is a {'b': 2,'aa': <function a.<locals>.aa at 0x107c58048>,'c': 3} this is aa {'aaa': <function a.<locals>.aa.<locals>.aaa at 0x107c58268>,'cc': 33,'bb': 22,'c': 3} this is aaa {'bbb': 222,'cc': 33,'ccc': 333,'c': 3} c: 3 cc: 33 g: 1 gg: 11

解説する前に、ちょっと視覚的にわかりやすく表現してみましょう。
aaa を基点として考えると、aaa がローカルスコープ, aa, a は エンクロージングスコープ です。

local より外側の関数が エンクロージングスコープ となり、 b,c, bb, cc は参照できます。もちろんg, ggも参照できますが、これらはグローバルスコープです。
大事なのは「定義された順番」ではなく「定義されているかどうか」だけです。ccは参照元の関数aaa より後で定義されていますが、呼出より前で定義されているため参照できます。
気になる人は、試しにaaa(222) より後でccを定義してみてください。「NameError: free variable ‘cc’ referenced before assignment in enclosing scope」こんなエラーが出るはずです。

また、b, bbがaaa関数のローカル変数となっていることに気づいたでしょうか。
これは自分も知らなかったんですが、どうやらエンクロージングスコープで参照した変数はローカル変数としても扱われるようです。

このエンクロージングスコープはクロージャとして利用されます。クロージャって何?って方はこの記事を最後まで読むかググってみましょう。

優先度

狭いスコープほど優先度が高い、最初にそんな話をしました。
Pythonのスコープは階層になっていると考えるとわかりやすいです。

例えば、こんなプログラムがあったとします。

id=1int=1 a =1 b =1   def outer(): id=2 a =2def inner(): id=3range=3# この記法ができるのは Python3.6 からprint(f'id:{id}, len:{len}, int:{int}, range:{range}, a:{a}, b:{b}') inner()

これを表に直すと以下のようになりました。(全部ではないけど)
表を上から覗き込み、一番上に該当する変数が参照されている様子がわかりますか。

Local33
Enclosing22
Global1111
Built-inid関数len関数int関数range関数
変数名idlenintrangeab

プログラムを実行すると表の通りに値が参照されていますね。

>>> outer()id:3,len:<built-in function len>,int:1,range:3, a:2, b:1

ちなみにどのスコープにも該当しない変数を参照するとNameErrorが発生します。

さて、もう少し続くんじゃ。

global文とnonlocal文

先程、関数内で定義された変数はすべてローカルスコープに属するという話をしました。
そうです。Pythonは代入文があると、加算代入だろうが利用者の意図と反してローカル変数だと判断されてしまうのです。
ローカル変数の優先度は一番強いので、存在していないローカル変数に加算して代入しようとしていると判断され UnboundLocalError が発生します。

g =1   def a(b): g += b print(g)
>>> a(2) Traceback (most recent call last): File "<stdin>", line 1,in<module> File "<stdin>", line 2,in a UnboundLocalError: local variable 'g' referenced before assignment

困りました。私はグローバル変数に 2 を足したいだけなのに UnboundLocalError って何なの?というときに使うのが global文です。

g =1   def c(d): global g g += d print(g)
>>> c(2)3>>> g 3

これで平和が訪れました。
global 文を使うことで関数スコープ中で変数への代入があっても、それはローカル変数ではなくグローバル変数であると解釈されます。

ちなみに UnboundLocalError とは ローカル変数として定義される予定だけど、まだ定義はされていない状態で参照しようとしたというNameErrorの一種です。
定義される予定ってなんだよって感じですね。Pythonは関数が定義された時点でその関数に属するローカル変数を決定します。ローカル変数が定義された瞬間ではありません。

一つ前の例で作った関数を見てみると

>>> a.__code__.co_varnames('b','g')>>> c.__code__.co_varnames('d',)# g は global文の指定により ローカル変数ではなくなった

こんなふうに、関数の外側からどんなローカル変数が定義されるか見えます。楽しいですね?

nonlocalも概ね同じですが、これが活躍するのはエンクロージングスコープの変数を書き換えたいとき、一番多いケースはやはりクロージャでしょう。

closure (クロージャ)

人によってはこの言葉を聞いたことがあると思います。というか聞いたことがある人はすでにわかったかもしれませんね。
クロージャとは変数を外側の関数、つまりエンクロージングスコープに閉じ込めた関数です。
こんな事をして何が嬉しいか?少し例を見てみます。数の合計を記憶する関数 pile について考えてみましょう。

def pile_factory(start=0): def pile(num): nonlocal start # nonlocalの指定が必須 start += num print(start)return pile

クロージャは外側と内側に定義された2つの関数からなり、内側の関数で外側の関数の変数を利用します。この変数を閉じ込める行為を束縛するとも言ったりもします。
外側の関数を返却する関数はその性質からファクトリ関数とも呼ばれ、今回の例ではpile_factoryがそれに当たります。

>>> pile1 = pile_factory(2)>>> pile2 = pile_factory(3)>>> pile1(3)5# 2 + 3>>> pile2(4)7# 3 + 4>>> pile1(4)9# 2 + 3 + 4>>> pile2(5)12# 3 + 4 + 5

重要なのは pile_factory によって作成された pile1, pile2 という関数それぞれが違う状態を保持しているということです。普通の関数は状態を持ちませんからね。
あとエンクロージングスコープに閉じ込められた変数はpile関数以外から参照することができません。この2点は大きなメリットです。

クロージャの応用にデコレータという技術があります。
デコレータについて知りたい方は こちら

少し話を戻します。
pile関数の中で、pile_factoryの引数(エンクロージングスコープの変数)を書き換えるため、nonlocalの指定が必要になります。
気になる方は nonlocal 文を外して UnboundLocalError が発生することを確認してみましょう。

この global文とnonlocal文、対象スコープ以外にもう一つ違いがあります。
global 文はまだグローバルスコープに定義されていないものも global文に指定できるのに対し、nonlocalはエンクロージングスコープに定義されていないものを指定できません。

g =1   def a(): global g, h print(g, h)     def b(): c =1def bb(): nonlocal c, d print(c, d)

こういうコードを書くと b 関数を定義した瞬間に 「SyntaxError: no binding for nonlocal ‘d’ found」と言われてしまいます。
グローバルスコープには後から変数を追加できますが、エンクロージングスコープには追加できませんから当然といえば当然の挙動ですね。
また global文 で同じ名前の変数を指定していると同様にSyntaxErrorになるようです。「SyntaxError: name ‘start’ is nonlocal and global」

でもSyntaxErrorて..

ちなみに a関数はこんな感じの挙動になります。

>>> a() Traceback (most recent call last): File "<stdin>", line 1,in<module> File "<stdin>", line 3,in a NameError: name 'h'isnot defined >>> h =2# h を定義したら..>>> a()12

これらの文について詳しく知りたい方は以下を参照

del 文

del という文を使うことで指定した変数を、参照可能な一番上のスコープから削除できます。

>>>print(range)<class'range'>>>>range=1>>>print(range)1>>>delrange>>>print(range)<class'range'>

この場合はビルトイン関数のrangeをマスクして(隠して)しまったグローバル変数を削除しています。

この考え方でいくと、ローカルスコープ→エンクロージングスコープ→グローバルスコープ→ビルトインスコープのように順に削除できそうに見えるんですが、ローカル変数として定義した時点で外側は見えないようでうまくいきません。また、ビルトインスコープの変数も del で消すことはできませんでした。
(やらないけど気になるやつ)

以上です。何かおかしいところがあったらやさしく教えてください。