Python Referenced Before Assignment In Enclosing Scope Of Work

Created on 2008-12-10 12:18 by amaury.forgeotdarc, last changed 2013-04-19 00:49 by barry. This issue is now closed.

File nameUploadedDescriptionEdit
delete_deref.patchamaury.forgeotdarc, 2008-12-10 12:18
msg77536 - (view)Author: Amaury Forgeot d'Arc (amaury.forgeotdarc) *Date: 2008-12-10 12:18
This issue comes from issue4613. The following code raises a SyntaxError("can not delete variable 'e' referenced in nested scope"): def f(): e = None def g(): e try: pass except Exception as e: pass # SyntaxError here??? The reason is because of http://www.python.org/dev/peps/pep-3110/#semantic-changes, a "del e" statement is inserted. The above code is correct, and should work. I suggest that the limitation: "can not delete variable referenced in nested scope" could be removed. After all, the "variable referenced" has no value before it is set, accessing it raises either NameError("free variable referenced before assignment in enclosing scope") or UnboundLocalError("local variable referenced before assignment") The Attached patch adds a DELETE_DEREF opcode, that removes the value of a cell variable, and put it in a "before assignment" state. Some compiler experts should review it. Few regressions are possible, since the new opcode is emitted where a SyntaxError was previously raised. The patch could also be applied to 2.7, even if it is less critical there. Tests are to come, but I'd like other's suggestions.
msg77691 - (view)Author: Terry J. Reedy (terry.reedy) *Date: 2008-12-12 23:03
-1 as I understand the proposal. Your code is bugged and should fail as soon as possible. If I understand correctly, you agree that the SyntaxError is correct as the language is currently defined, but you want the definition changed. It is not clear if you only want implicit deletes at the end of except clauses to work or if you only want explicit deletes to work. If the latter, you want def f(): e = 1 del e def g(): print(e) return g to compile. I would not. Your reason "After all, the "variable referenced" has no value before it is set," (duh, right) makes no sense to me in this context. g must have a valid value of e to run. So you seem to be suggesting that detection of buggy code should be delayed.
msg77693 - (view)Author: Raymond Hettinger (rhettinger) *Date: 2008-12-12 23:29
Not sure the "del e" idea was a good solution to the garbage collection problem. Amaury's code looks correct to me. Maybe the existing e variable should be overwritten and the left intact (as it used to be) or perhaps it should be made both temporary and invisible like the induction variable in a list comprehension. Phillip, any thoughts?
msg77696 - (view)Author: Amaury Forgeot d'Arc (amaury.forgeotdarc) *Date: 2008-12-12 23:49
Terry, my motivation is that the sample code above runs correctly with python 2.6, but python 3.0 cannot even compile it. The sample looks valid python code, and should run. Yes, the same 'e' is used both as a nested variable and as an exception target, but this should not matter with our dynamic language. First I thought to turn the implicit "del e" into something else (and change PEP3110), but then I saw that the error "can not delete variable referenced in nested scope" is actually a limitation of the interpreter that is easy to remove.
msg77703 - (view)Author: PJ Eby (pje) *Date: 2008-12-13 00:37
I could argue either way on this one; it's true that deleting a nested-scope variable is sometimes desirable, but it also seems to me like closing over an except: variable is a Potentially Bad Idea. In neither case, however, do I think it's appropriate to drop the temporary nature of the variable. I could perhaps get behind resetting the variable to None instead of deleting it, but of course the PEP would need changing. There's also a question of whether we should do the same thing with "with ... as" variables. (Btw, I'm not sure why this one's assigned to me; ISTM I might have proposed the current except/as GC semantics, but I'm not familiar with the actual implementation in 2.6 or 3.0)
msg77704 - (view)Author: Raymond Hettinger (rhettinger) *Date: 2008-12-13 00:48
Guido, any thoughts?
msg78434 - (view)Author: Benjamin Peterson (benjamin.peterson) *Date: 2008-12-28 21:28
I think being able to delete free variables is reasonable and brings more consistency as well as solving corner cases like this.
msg79228 - (view)Author: Guido van Rossum (gvanrossum) *Date: 2009-01-06 05:01
I don't think this has much to do with try/except. That it works in 2.6 but not in 3.0 isn't a big deal; the semantics of variables used in except clauses has changed dramatically. It has to do with deletion of a variable that's held in a cell for reference by an inner function, like this: def outer(): x = 0 def inner(): return x del x # SyntaxError I suspect (but do not know for sure) that the reason this is considered a SyntaxError is that the implementer of cells punted on the 'del' implementation and inserted a SyntaxError instead. (You can tell it's a pass-two SyntaxError because it doesn't mention a line number.) I think it's fine to fix this in 2.7 and 3.1, but I don't see it as a priority given that this has always been this way (and despite that it now affects try/except). It will probably require a new opcode. I don't see a reason to declare this a release blocker just because the try/except code is affected, and I don't think try/except needs to be changed to avoid this.
msg99247 - (view)Author: Craig McQueen (cmcqueen1975)Date: 2010-02-12 02:09
There's also this one which caught me out: def outer(): x = 0 y = (x for i in range(10)) del x # SyntaxError
msg99855 - (view)Author: Jeremy Hylton (jhylton)Date: 2010-02-22 22:18
It's an interesting bug. Maybe the compiler shouldn't allow you to use such a variable as a free variable in a nested function? On Thu, Feb 11, 2010 at 9:09 PM, Craig McQueen <report@bugs.python.org> wrote: > > Craig McQueen <python@craig.mcqueen.id.au> added the comment: > > There's also this one which caught me out: > > def outer(): >  x = 0 >  y = (x for i in range(10)) >  del x  # SyntaxError > > ---------- > nosy: +cmcqueen1975 > > _______________________________________ > Python tracker <report@bugs.python.org> > <http://bugs.python.org/issue4617> > _______________________________________ > _______________________________________________ > Python-bugs-list mailing list > Unsubscribe: http://mail.python.org/mailman/options/python-bugs-list/jeremy%40alum.mit.edu > >
msg99866 - (view)Author: Guido van Rossum (gvanrossum) *Date: 2010-02-22 23:10
All examples so far (*) have to do with our inability to have properly nested blocks. If we did, I'd make the except clause a block, and I'd issue a syntax warning or error if a nested block shadowed a variable referenced outside it. Ditto for generator expressions and comprehensions. As long as we don't have nested blocks, I think it's okay to see the limitation on (implicit or explicit) "del" of a cell variable as a compiler deficiency and fix that deficiency. __________ (*) However there's also this example: >>> def f(): ... try: 1/0 ... except Exception as a: ... def g(): return a ... return g ... SyntaxError: can not delete variable 'a' referenced in nested scope >>>
msg99880 - (view)Author: Jeremy Hylton (jhylton)Date: 2010-02-22 23:51
On Mon, Feb 22, 2010 at 6:10 PM, Guido van Rossum <report@bugs.python.org> wrote: > > Guido van Rossum <guido@python.org> added the comment: > > All examples so far (*) have to do with our inability to have properly nested blocks. If we did, I'd make the except clause a block, and I'd issue a syntax warning or error if a nested block shadowed a variable referenced outside it. Ditto for generator expressions and comprehensions. There's no reason we couldn't revise the language spec to explain that except clauses and comprehensions are block statements, i.e. statements that introduce a new block. For the except case, there would be some weird effects. y = 10 try: ... except SomeError as err: y = 12 print y # prints 10 In the example above, y would be a local variable in the scope of the except handler that shadows the local variable in the block that contains the try/except. It might be confusing that you couldn't assign to a local variable in the except handler without using a nonlocal statement. > As long as we don't have nested blocks, I think it's okay to see the limitation on (implicit or explicit) "del" of a cell variable as a compiler deficiency and fix that deficiency. The general request here is to remove all the SyntaxErrors about deleting cell variables, right? Instead, you'd get a NameError at runtime saying that the variable is currently undefined. You'd want that change regardless of whether we change the language as described above. hoping-for-some-bdfl-pronouncements-ly y'rs, Jeremy > __________ > (*) However there's also this example: > >>>> def f(): > ...  try: 1/0 > ...  except Exception as a: > ...   def g(): return a > ...   return g > ... > SyntaxError: can not delete variable 'a' referenced in nested scope >>>> > > ---------- > > _______________________________________ > Python tracker <report@bugs.python.org> > <http://bugs.python.org/issue4617> > _______________________________________ >
msg99911 - (view)Author: Guido van Rossum (gvanrossum) *Date: 2010-02-23 13:43
On Mon, Feb 22, 2010 at 6:51 PM, Jeremy Hylton <report@bugs.python.org> wrote: > There's no reason we couldn't revise the language spec to explain that > except clauses and comprehensions are block statements, i.e. > statements that introduce a new block. However (even apart from the below example) it would be tough to implement cleanly in CPython. > For the except case, there would be some weird effects. > > y = 10 > try: >  ... > except SomeError as err: >  y = 12 > print y  # prints 10 > > In the example above, y would be a local variable in the scope of the > except handler that shadows the local variable in the block that > contains the try/except.  It might be confusing that you couldn't > assign to a local variable in the except handler without using a > nonlocal statement. Yeah, there are all sorts of problems with less-conspicuous nested scopes like this, for a language that defaults to local assignment like Python. Hence the horrible hacks. >> As long as we don't have nested blocks, I think it's okay to see the limitation on (implicit or explicit) "del" of a cell variable as a compiler deficiency and fix that deficiency. > > The general request here is to remove all the SyntaxErrors about > deleting cell variables, right?  Instead, you'd get a NameError at > runtime saying that the variable is currently undefined.  You'd want > that change regardless of whether we change the language as described > above. Yeah, if we could kill those SyntaxErrors we can leave the rest as is.
msg99915 - (view)Author: Amaury Forgeot d'Arc (amaury.forgeotdarc) *Date: 2010-02-23 13:52
The above patch adds a new opcode (DELETE_DEREF), does the Moratorium apply here?
msg99918 - (view)Author: Guido van Rossum (gvanrossum) *Date: 2010-02-23 14:38
I don't think so. It's very marginal. --Guido (on Android) On Feb 23, 2010 8:52 AM, "Amaury Forgeot d&apos;Arc" <report@bugs.python.org> wrote: Amaury Forgeot d'Arc <amauryfa@gmail.com> added the comment: The above patch adds a new opcode (DELETE_DEREF), does the Moratorium apply here? ---------- _______________________________________ Python tracker <report@bugs.python.org> <http://bugs.python...
msg99950 - (view)Author: Jeremy Hylton (jhylton)Date: 2010-02-23 20:41
The patch looks pretty good. I'd factor out the common error-checking code (common between LOAD_DEREF and DELETE_DEREF) into a helper function. It would also be good to add some test cases. Jeremy On Tue, Feb 23, 2010 at 9:38 AM, Guido van Rossum <report@bugs.python.org> wrote: > > Guido van Rossum <guido@python.org> added the comment: > > I don't think so. It's very marginal. > > --Guido (on Android) > > On Feb 23, 2010 8:52 AM, "Amaury Forgeot d&apos;Arc" <report@bugs.python.org> > wrote: > > Amaury Forgeot d'Arc <amauryfa@gmail.com> added the comment: > > The above patch adds a new opcode (DELETE_DEREF), does the Moratorium apply > here? > > ---------- > > _______________________________________ > Python tracker <report@bugs.python.org> > <http://bugs.python... > > ---------- > Added file: http://bugs.python.org/file16341/unnamed > > _______________________________________ > Python tracker <report@bugs.python.org> > <http://bugs.python.org/issue4617> > _______________________________________
msg113312 - (view)Author: Florent Xicluna (flox) *Date: 2010-08-08 20:22
This bug is waiting for unit tests and a small patch cleanup. See previous message: http://bugs.python.org/issue4617#msg99950
msg113335 - (view)Author: Terry J. Reedy (terry.reedy) *Date: 2010-08-08 21:58
I have changed my mind on this issue. Since e = 1 del e def g(): print(e) g() compiles and raises a run-time name error, so should the same code embedded within a function. In either case, the premature deletion is a logic error, not a syntax error. However, changing the language definition, even to fix what is considered a design bug, is a feature request. For both 2.7 and 3.1, section 6.5. "The del statement", says "It is illegal to delete a name from the local namespace if it occurs as a free variable in a nested block." So this seems too late for 2.7. On the other hand, Guido has allowed it for 3.2 in spite of the moratorium, but I think it should go in the initial release.
msg113340 - (view)Author: Guido van Rossum (gvanrossum) *Date: 2010-08-08 22:10
Yeah, please fix in 3.2, don't fix in 2.7.
msg116048 - (view)Author: Amaury Forgeot d'Arc (amaury.forgeotdarc) *Date: 2010-09-10 22:02
Fixed in r84685, with tests and doc updates.
DateUserActionArgs
2013-04-19 00:49:14barrysetnosy: + barry
2010-09-10 22:02:17amaury.forgeotdarcsetstatus: open -> closed
resolution: accepted -> fixed
messages: + msg116048

stage: test needed -> resolved
2010-09-10 14:44:14amaury.forgeotdarcsetassignee: amaury.forgeotdarc
resolution: accepted
2010-08-09 06:16:06scodersetnosy: - scoder
2010-08-08 22:10:44gvanrossumsetmessages: + msg113340
2010-08-08 21:58:04terry.reedysettype: behavior -> enhancement
messages: + msg113335
versions: - Python 2.7
2010-08-08 20:22:38floxsetversions: + Python 3.2, - Python 3.0
nosy: + scoder, ezio.melotti

messages: + msg113312

keywords: - needs review
2010-03-13 08:50:33floxsetnosy: + flox

type: behavior
components: + Interpreter Core
stage: test needed
2010-03-13 08:50:00floxsetfiles: - unnamed
2010-03-13 08:48:44floxlinkissue8130 superseder
2010-02-23 20:41:28jhyltonsetmessages: + msg99950
2010-02-23 14:38:31gvanrossumsetfiles: + unnamed

messages: + msg99918
2010-02-23 13:52:22amaury.forgeotdarcsetmessages: + msg99915
2010-02-23 13:43:28gvanrossumsetmessages: + msg99911
2010-02-22 23:51:09jhyltonsetmessages: + msg99880
2010-02-22 23:10:52gvanrossumsetmessages: + msg99866
2010-02-22 22:18:06jhyltonsetmessages: + msg99855
2010-02-12 02:09:11cmcqueen1975setnosy: + cmcqueen1975
messages: + msg99247
2009-01-06 05:01:44gvanrossumsetpriority: release blocker -> normal
assignee: gvanrossum -> (no value)
messages: + msg79228
2008-12-28 21:28:21benjamin.petersonsetnosy: + benjamin.peterson
messages: + msg78434
2008-12-20 02:41:17loewissetpriority: deferred blocker -> release blocker
2008-12-13 00:48:42rhettingersetassignee: pje -> gvanrossum
messages: + msg77704
nosy: + gvanrossum
2008-12-13 00:37:22pjesetmessages: + msg77703
2008-12-12 23:49:05amaury.forgeotdarcsetmessages: + msg77696
2008-12-12 23:29:12rhettingersetassignee: pje
messages: + msg77693
nosy: + pje, rhettinger
2008-12-12 23:03:43terry.reedysetnosy: + terry.reedy
messages: + msg77691
2008-12-10 18:46:53benjamin.petersonsetnosy: + jhylton
2008-12-10 16:41:24loewissetpriority: release blocker -> deferred blocker
2008-12-10 12:18:42amaury.forgeotdarccreate

お久しぶりです。
最近は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 で消すことはできませんでした。
(やらないけど気になるやつ)

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

0 Thoughts to “Python Referenced Before Assignment In Enclosing Scope Of Work

Leave a comment

L'indirizzo email non verrà pubblicato. I campi obbligatori sono contrassegnati *