Python
iRidgeDay 16

Pythonの変数スコープの話

Pythonは変数のスコープが曖昧な事を忘れない

たまに忘れてハマるので、覚書ついでに共有です。

グローバル変数とローカル変数

Pythonは関数内で変数を呼び出した時、グローバルですでに定義されていた場合はグローバル変数が使用されます。

global_var = "Global Varibale"
def get_global():
    local_var = global_var
    return local_var
print(get_global())

>>> Global Variable

この時、呼び出し後に関数内で代入処理を行った場合、ローカルのスコープとして扱われ例外が発生します。
(Pythonは関数内で代入が行われると、初めてローカルスコープとしてその変数を取り扱います)

global_var = "Global Varibale"
def get_global():
    local_var = global_var
    global_var = "Reset Local" # Add
    return local_var
print(get_global())

>>> Traceback (most recent call last):
>>>   File "test.py", line 7, in <module>
>>>     print(get_global())
>>>   File "test.py", line 3, in get_global
>>>     local_var = global_var
>>> UnboundLocalError: local variable 'global_var' referenced before assignment

グローバル変数を使用していると思い、グローバル変数を上書きするつもりで代入処理を行うと例外になるためスコープ外の変数を扱う場合は注意が必要です。

これを防ぐために、グローバル変数で使用されている(もしくは使用される可能性がある変数名を指定する)場合は、関数定義直後に初期化を行うことをおすすめします。

common_var = "Global Varibale"
def get_global():
    common_var = ""
    # Intermediate processing
    common_var = "Local Variable"
    return common_var

print(get_global())
print(common_var)

>>> Local Variable
>>> Global Varibale

また、グローバル変数を関数内で使用する場合は、 global で定義することで明示的にグローバル変数を使用することができます。

common_var = "Global Varibale"
def get_global():
    global common_var
    # Intermediate processing
    common_var = "Local Variable"
    return common_var

print(get_global())
print(common_var)

>>> Local Variable
>>> Local Variable

クラスで定義する変数

  • 追記
    • 以下のグローバルスコープは インスタンスからみてグローバル = クラス変数 という意味です。
    • 逆に、ローカルスコープは インスタンスから見て閉じている = インスタンス変数 という意味です。
    • 上の記述と合わせるために文言を統一しました。

 
* クラスに定義した変数をインスタンス内で再度代入する。

class TestClass(object):
    common_var = "Initial Value"
    def get_variable(self):
        return self.common_var
    def set_variable(self, value):
        self.common_var = value

tc1 = TestClass()
tc2 = TestClass()
tc1.set_variable("Set Variable")
print(tc2.common_var)

>>> Initial Value

この場合、値を代入することでスコープがインスタンス内に限定され、 tc2 の変数は上書きされません。
(関数と同様にメソッド内で代入処理を行うと、ローカルスコープとして取り扱われます。 クラス変数として操作したい場合は、クラスを指定する必要があります。)

しかし、これがMutableな変数であり、代入処理が行われないとグローバルスコープで処理され tc2 へも影響を及ぼします。

class TestClass(object):
    common_var = []
    def get_variable(self):
        return self.common_var
    def set_variable(self, value):
        self.common_var.append(value)

tc1 = TestClass()
tc2 = TestClass()
tc1.set_variable("Set Variable")
print(tc2.common_var)

>>> ['Set Variable']

これらの動作は、Pythonは元々オブジェクト指向の設計ではなく、前述の話が理解できていればある程度想定はできる事象だと思います。
(とはいえ、普通は考えにくい動作なので忘れてるとハマります。)
指摘頂いたので修正します。
common_varが クラス変数である という前提であればなんらおかしい動作ではありませんが、前述の動作を考慮すると分かりにくい動作になっています。

しかし、メソッドのキーワード引数に対してMutableな値を初期値として渡した場合も、初期値として渡しているにもかかわらずグローバルスコープで取り扱われるという仕様があります。
(初期値として渡しているので前述の理論からはローカルスコープになりそうですが、そうはなりません)

class TestClass(object):
    def __init__(self):
        self.common_var = None
    def set_variable(self, values=[]):
        values.append("Set Variable")
        self.common_var = values

tc1 = TestClass()
tc2 = TestClass()
tc1.set_variable()
tc2.set_variable()
print(tc2.common_var)

>>> ['Set Variable', 'Set Variable']

通常であれば、 tc2.set_variable() は一度しか呼ばれていないためリストの中身は一つだけであることを期待しますが、 tc1.set_variable() でappendしたデータも含まれてしまっています。
以上から、キーワード引数の評価はクラスが呼ばれた後一度しか行われず、その後はグローバルスコープとして取り扱われるということがわかります。
(評価されない=初期化されないので、前述の理論からグローバルスコープとして取り扱われることがわかります。)

このように、Pythonは変数のスコープが曖昧であるため、特にMutableな変数(代入が行われずに使用されることが想定される)の場合は初期化を、クラス変数を取り扱う場合はクラスを指定して操作することを意識する必要があります。