[Python]スコープの基本概念とUnboundLocalErrorの回避方法

スコープは実際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は関数定義の一部であり、あくまでローカルスコープです。関数が呼び出されたときの挙動を細かく確認していきましょう。

  1. power(5)と呼び出される
  2. 関数ヘッダでx = 5という処理が行われる(ローカル変数が作成される)
  3. 関数内部では「xをx乗する」という処理が定義されているため、関数はローカルスコープでxを検索する
  4. ヘッダから「xは5だよ」という返答が返る
  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」という変数は関数のローカルスコープにしか存在しません。このため、グローバルスコープのコードで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 *= xy = x * xに変更されていますね。これはうっかりです。直しましょう。

x = 3

def power():
    x *= x
    return x

print(power())

これで1つ目のコードとそっくり。しかし実行してみると、

#UnboundLocalError: local variable 'x' referenced before assignment
束縛されていないローカル変数のエラー: ローカル変数xは、値が代入される前に参照された

(゚Д゚)ハァ?

値が代入されていない?

代入されていない理由

なぜ「xに値が代入されていない」と怒られるのか。「1行目でちゃんと代入してるじゃないか」と思われるかもしれませんが、問題は1行目ではありません。

なぜ怒られるのか。一言で言ってしまうと、関数内で代入してしまっているからです。

上で書いたことの繰り返しになりますが、関数は内部で未知の変数を発見した場合、

  1. 関数内部で定義されていない
  2. 仮引数として受け取っていない

ことを条件にスコープをさかのぼります。それをしっかりと把握した上で、もう一度問題の箇所を見てください。

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
文法エラー: xという名前は関数のパラメータ(仮引数)であり、同時にグローバル指定されている

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です。これは関数が内部でグローバル変数に新たな値を代入し、それが関数終了後も持ち越されてしまうために起こります。

そもそも関数を定義するということはスコープを区切り、予期しない変更から変数を守るという側面もあります。可能な限り関数が無条件にグローバル変数を参照できるようなコードを書くべきではありません。

まとめ

今回のまとめです。

  1. ファイルで区切った範囲をグローバルスコープ、関数やクラス定義などで区切った範囲をローカルスコープと呼ぶ
  2. グローバルスコープからはローカルスコープの変数が見えず、ローカルスコープからはグローバルスコープの変数が見える
  3. ローカルスコープからはグローバル変数が見えるが、値を直接代入することはできない
  4. そのため変数への意図しない代入を防ぎ、グローバル変数は関数やその他の処理から保護されている

タイトルとURLをコピーしました