7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Python] クラス変数をもつクラスを継承する

Last updated at Posted at 2020-07-04

ポイント:

  • 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のように,「共有すべきクラス変数」「子クラスで上書きすべきクラス変数」を明示できるようにできれば…

現状の対策としては,

  • 継承されることが前提の抽象クラスにはクラス変数を定義しない(上記の通り結局子クラスで再定義する方が安全なので単純に行数の無駄)
  • そうでないクラスのクラス変数にアクセスする必要がある時は,継承ではなく委譲を使う(理想)
  • 継承するクラスのソースコードは冒頭だけでも読む(現実)

ぐらいでしょうか….何か良い方策などございましたらご教示頂ければ幸いです.

7
5
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?