ポイント:
-
self.変数名
でアクセスすると,インスタンス変数→クラス変数→親クラスのクラス変数の順に探索する -
self.辞書[キー] = 値
は新たな辞書を作らず,self.辞書
を探索する-
self.辞書
というインスタンス変数がない場合,クラス変数や親クラスのクラス変数を参照するため,それらが(意図せず)変更される可能性がある - 新たに辞書を作るには,
self.辞書 = {キー: 値}
-
2020/07/06追記: @shiracamusさんのコメントを受けて内容を修正.
症状
# 親クラス
class AbstClass:
class_number = 0
dict_number = {'class': 0}
# 子クラス1
class Class1(AbstClass):
def __init__(self):
self.class_number = 1
self.dict_number['class'] = 1
# 子クラス2
class Class2(AbstClass):
def __init__(self):
self.class_number = 2
self.dict_number['class'] = 2
obj1 = Class1()
print(obj1.class_number, obj1.dict_number['class']) # 1, 1 ←わかる
obj2 = Class2()
print(obj2.class_number, obj2.dict_number['class']) # 2, 2 ←わかる
print(obj1.class_number, obj1.dict_number['class']) # 1, 2 ←!?
「2つの子クラスが同じ親クラスのクラス変数にアクセスしている」なら分かりやすいが,
上記の例では
-
class_number
は2つのクラスで別々に管理 -
dict_number
は2つのクラスで共有→Class2()をしたときにClass1で書き換えたdict_numerを上書き
になっている.
対応
辞書[キー] = 値
は元々ある辞書を探索
class Class1(AbstClass):
def __init__(self):
self.class_number = 1 # 新たなインスタンス変数class_numberが作られる
self.dict_number['class'] = 1 # インスタンス変数にdict_numberがないので,
# Class1のクラス変数→AbstClassのクラス変数を探索
# AbstClassのdict_numberが更新される
という動作になっている.
「どうしてself.dict_numberは新しく辞書を作らないのだろう?」について,
Qiita@ponnhide: pythonの参照についてに,
変数に新たなオブジェクトそのものが代入されるときには、これまでの参照先から変わって新たなオブジェクトが作られた場所を参照するようになる
とある.従って,上記self.dict_number['class'] = 1
の代わりに
self.dict_number = {'class': 1}
とすると,他のクラス変数に影響を与えずに新たなインスタンス変数dict_number
が作られる.
クラス変数を明示して代入するにはtype(self).クラス変数名
class Class1:
class_number = 0
def __init__(self):
self.class_number = 1 # インスタンス変数(そのインスタンスにのみ保持)
obj1 = Class1()
print(obj1.class_number) # 1
print(Class1.class_number) # 0
class Class1:
class_number = 0
def __init__(self):
type(self).class_number = 1 # クラス変数に代入される
obj1 = Class1()
# インスタンスを作ったあとには
# type(obj1).class_number = 1
print(obj1.class_number) # 1
print(Class1.class_number) # 1
原因の仮説
mutable/immutableが関係している?と推測.つまり,
mutableなクラス変数(上記のdict_number
): 子クラスでも共有immutableなクラス変数(上記のclass_number
): 子クラスでは別々に管理
実際,AbstClassにclass_number_tuple = (0,)
というタプルを定義すると,
class_number
と同様の振る舞いが確認された.
(※intはimmutable.参考:
[GAMMASOFT: Pythonの組み込みデータ型の分類表(ミュータブル等)]
(https://gammasoft.jp/blog/python-built-in-types/))
対策
子クラス内でクラス変数を再定義する:
class Class1(AbstClass):
class_number = 0
dict_number= {'class': 0}
class Class2(AbstClass):
class_number = 0
dict_number= {'class': 0}
と,各インスタンスではそれぞれのクラスのクラス変数を優先する=別々に管理することになる.
ただし,欲を言えば言語として自動的に例外を出してほしい.
実際,辞書のキーにリストを入れるとunhashableと怒られるABCの@abstractmethod
のように,「共有すべきクラス変数」「子クラスで上書きすべきクラス変数」を明示できるようにできれば…
現状の対策としては,
継承されることが前提の抽象クラスにはクラス変数を定義しない(上記の通り結局子クラスで再定義する方が安全なので単純に行数の無駄)そうでないクラスのクラス変数にアクセスする必要がある時は,継承ではなく委譲を使う(理想)継承するクラスのソースコードは冒頭だけでも読む(現実)
ぐらいでしょうか….何か良い方策などございましたらご教示頂ければ幸いです.