スコープは実際4種類ありますが、今回は理解しやすいグローバルスコープとローカルスコープの基本概念、そしてありがちなUnboundLocalErrorを回避するglobalキーワードについてお話します。
スコープとは「範囲」である
スコープ(scope)は和訳すると範囲ですが、プログラミング言語で言えば「名前を読み書きできる」範囲です。
特に変数について見ていきましょう。
x = 1
def power(x):
x *= x
return x
print(x) #1
print(power(5)) #25
print(x) #1
このコードの変数は「x」のみです。しかし最初のxと関数内で使用されているxは全く違うものとして扱われています。これはスコープが違うからです。
最初のxをグローバル変数(グローバルスコープの変数)、関数内のxをローカル変数(ローカルスコープの変数)と呼びます。
グローバルスコープとローカルスコープ
グローバルスコープとは簡単に言うと、「1ファイルを囲った範囲」です。
コードの1行目に書かれたx = 1
は、当然ですが.pyファイルに書かれています。ファイルの中にはこの変数xを囲むような「範囲」はありません。ファイルそのものが大きな1つの範囲と言えます。これがグローバルスコープです。
一方で関数は、def 関数名():
によって範囲が定められます。「ここからここまでは関数です」と枠を作るわけです。
クラス定義や制御構文も同じですが、この枠に囲まれた部分をローカルスコープと呼びます。
引数はローカルスコープ
次に関数定義を見てみます。
def power(x):
x *= x
return x
今回の例では関数が引数として「x」を受け取っています。しかしこのxは関数定義の一部であり、あくまでローカルスコープです。関数が呼び出されたときの挙動を細かく確認していきましょう。
power(5)
と呼び出される- 関数ヘッダで
x = 5
という処理が行われる(ローカル変数が作成される) - 関数内部では「xをx乗する」という処理が定義されているため、関数はローカルスコープでxを検索する
- ヘッダから「xは5だよ」という返答が返る
- 5を5乗した「25」がreturnされる
では引数が無い関数の場合はどうなるでしょう?
ローカルスコープからはグローバルスコープが見える
x = 3
def power():
y = x * x
return y
print(x) #3
print(power()) #9
print(x) #3
関数内部で使用されている変数(ここではx)が、関数内の処理でも引数でも定義されていない場合、関数はスコープをさかのぼってxを探しに行きます。この場合、スコープをさかのぼった先はグローバルスコープです。
グローバルスコープでxが見つかると、関数はそれを使って処理を行います。
つまりローカルスコープからはグローバルスコープの変数が見えるということになります。通常の場合、逆はあり得ません。
x = 3
def power():
y = x * x
return y
print(y) #NameError: name 'y' is not defined
「y」という変数は関数のローカルスコープにしか存在しません。このため、グローバルスコープのコードでyを参照しようとしても「定義されていない」というエラーになります。
「内側からは外側の変数が見える(外側の変数を使える)が、外側から内側の変数は見えない」というのがPythonのスコープの基本です。
UnboundLocalErrorを回避する
よくあるエラー
もう一度コード全体を思い出してみましょう。まずは最初に示したコード。
x = 1
def power(x):
x *= x
return x
print(x) #1
print(power(5)) #25
print(x) #1
続いて2つ目のコードです。見比べてみてください。
x = 3
def power():
y = x * x
return y
print(x) #3
print(power()) #9
print(x) #3
1行目のxの数値や引数以外に何か変わってしまっている箇所があります。よく見ると関数の処理の部分で、x *= x
がy = x * x
に変更されていますね。これはうっかりです。直しましょう。
x = 3
def power():
x *= x
return x
print(power())
これで1つ目のコードとそっくり。しかし実行してみると、
#UnboundLocalError: local variable 'x' referenced before assignment
(゚Д゚)ハァ?
値が代入されていない?
代入されていない理由
なぜ「xに値が代入されていない」と怒られるのか。「1行目でちゃんと代入してるじゃないか」と思われるかもしれませんが、問題は1行目ではありません。
なぜ怒られるのか。一言で言ってしまうと、関数内で代入してしまっているからです。
上で書いたことの繰り返しになりますが、関数は内部で未知の変数を発見した場合、
- 関数内部で定義されていない
- 仮引数として受け取っていない
ことを条件にスコープをさかのぼります。それをしっかりと把握した上で、もう一度問題の箇所を見てください。
def power():
x *= x
return x
「*=」演算子の元の形は何だったでしょうか?
そう、「x = x*x」です。
…代入、してますよね。
これは関数のルールとして正しい挙動です。しかしそうじゃないんだPython。ここのxはグローバルスコープのxなんだという場合、「globalキーワード」を使うことで、明示的にグローバル変数を指定することができます。
globalキーワード
使い方はすごく簡単。この関数に1行追加するだけです。
def power():
global x
x *= x
return x
globalキーワードは「この変数が出てきたら、それはグローバル変数を表す」という意味で使用します。
これによってこの関数は内部でxが定義されているにも関わらず、その代入を無視してグローバルスコープの変数を参照しに行きます。
ただし引数としてこの変数を受け取った場合、さすがにエラーになるので注意しましょう。
def power(x):
global x
x *= x
return x
print(power(5)) #SyntaxError: name 'x' is parameter and global
globalキーワードの副作用
しかしこのglobalキーワードの使用はあまりオススメしません。なぜならこのコードには致命的な副作用が存在するからです。
x = 3
def power():
global x
x *= x
return x
print(x) #3
print(power()) #9
print(x) #9
最後の3行を見てください。関数呼び出し前のprint(x)
は3ですが、最後のprint(x)
は9です。これは関数が内部でグローバル変数に新たな値を代入し、それが関数終了後も持ち越されてしまうために起こります。
そもそも関数を定義するということはスコープを区切り、予期しない変更から変数を守るという側面もあります。可能な限り関数が無条件にグローバル変数を参照できるようなコードを書くべきではありません。
まとめ
今回のまとめです。
- ファイルで区切った範囲をグローバルスコープ、関数やクラス定義などで区切った範囲をローカルスコープと呼ぶ
- グローバルスコープからはローカルスコープの変数が見えず、ローカルスコープからはグローバルスコープの変数が見える
- ローカルスコープからはグローバル変数が見えるが、値を直接代入することはできない
- そのため変数への意図しない代入を防ぎ、グローバル変数は関数やその他の処理から保護されている