背景
中学生のK君(二回目の登場)が思ったように動かず、泣きついてきました。
直してあげたんだけど、nonlocalの事でもやもやが残りました。
もやもや解消したので残しておきます。
動かないプログラム
なにが原因か探してみてください。
import tkinter as tk
count = 0
def dispLabel():
count = count + 1
lbl.configure(text = str(count) + "回押しました")
root = tk.Tk()
root.geometry("300x80")
lbl = tk.Label(text="数えるボタン")
btn = tk.Button(text="押してね", command = dispLabel)
lbl.pack()
btn.pack()
tk.mainloop()
ボタンが表示されてボタンを押すと、エラーが出ます。
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Users\masa\Anaconda3\lib\tkinter\__init__.py", line 1705, in __call__
return self.func(*args)
File "c:\Users\masa\OneDrive\ドキュメント\python\lessen_tk.py", line 6, in dispLabel
count = count + 1
UnboundLocalError: local variable 'count' referenced before assignment
スコープ、ローカル変数を理解してないと間違える基本的な間違いですね。
dispLabel()の中でグローバルにあるcountをインクリメントしたいのですが、dispLabel()の中でcountに値を代入部分があると、countはローカル変数と認識され、global変数の参照ができなくなります(countへの代入がなければ、globalの参照できます)このため、値を参照すると、値が代入される前なのでエラーになる(詳しくは、おまけを見てください)。
直してみる
説明するのもダルイので、Pythonチュートリアルの例を見て、法則を理解してもらった。
def scope_test():
def do_local():
spam = "local spam"
def do_nonlocal():
nonlocal spam
spam = "nonlocal spam"
def do_global():
global spam
spam = "global spam"
spam = "test spam"
do_local()
print("After local assignment:", spam)
do_nonlocal()
print("After nonlocal assignment:", spam)
do_global()
print("After global assignment:", spam)
scope_test()
print("In global scope:", spam)
関数の中の変数は外側とは、別物扱いされると理解したK君、
チュートリアルを真似して次のように直しました。
def dispLabel():
nonlocal count
count = count + 1
lbl.configure(text = str(count) + "回押しました")
実行結果
File "c:\Users\masa\OneDrive\ドキュメント\python\lessen_tk.py", line 6
nonlocal count
^
SyntaxError: no binding for nonlocal 'count' found
T君:「nonlocalか、globalつければいいんですよね。でもできないなぁ」
私:なんだこれ??「global countにすれば動くよ。わかんないね。」
とお茶を濁す。(説明できなくてごめん)
当然、globalにしたら動きました。
nonlocalだとなぜダメ?
nonlocalは一つ外側の名前空間の変数を参照できるようにするものなのにどうしてダメなんだろう。nonlocal知ってはいたけど、実際に使う場面もなかった。
pythonのドキュメント
nonlocal 文は、列挙された識別子がグローバルを除く一つ外側のスコープで先に束縛された変数を参照するようにします。これは、束縛のデフォルトの動作がまずローカル名前空間を探索するので重要です。この文は、中にあるコードが、グローバル (モジュール) スコープ以外のローカルスコープの外側の変数を再束縛できるようにします。
nonlocal 文で列挙された名前は、 global 文で列挙された名前と違い、外側のスコープですでに存在する束縛を参照しなければなりません (新しい束縛が作られるべきスコープの選択が曖昧さを排除できません)。
原因まとめ
- この例では、dispLabel()スコープの1つ外側のcountはグローバル。このため、nonlocalの対象にはならない。
- globalは、実行すれば変数が作成されるけど、nonlocalは、作成する前に、存在していないといけない
おまけ
関数の中で、globalの宣言をしていなくてもglobalの変数countを読みだすだけならglobalの宣言はいりません。
しかし、countへの代入が存在すると、countはローカル変数と認識されます。
注意点、count=が実行されたかどうかではなく、count=の行が存在するかで判断されるようです。
次の場合でもcountは、ローカル変数と認識されて、a=の行でreferenced before assignmentの例外が発生します(理解できてなかったのでやってみたらこんな結果でした)。
def dispLabel():
a = count + 1
if False:
count=a
lbl.configure(text = str(count) + "回押しました")