Pythonのsuperについて調べた(mroとメタクラス)
前置き
この記事はsuper
の実践的な使い方を紹介するような記事ではなく、個人的に気になっていたsuper
のよく分からない(おそらく非実践的な)挙動を調べてみたのでまとめた記事です。
継承の基本
まずは継承の基本的な事柄について再確認します。すでに分かっている場合はこの章を読み飛ばしてもらっても問題ありません。ここでは一般的なsuper
の使い方を説明します。
では早速クラスを継承してsuper
を使ってみましょう。
以下では親クラスにはPar
という名前を、子クラスにはChi
という名前を優先的に使うこととします(ParentとChildより)1。しかし、多重継承やメタクラスを扱う関係上、これに従うことは難しいと判断した場合はクラス名にA
だとかB
だとか使い始めると思います。
class Par:
def f(self):
print("Par")
class Chi(Par):
def f(self):
super().f()
Chi().f() # Par
特に言うことはない、super
を使用する最小単位の例ですね。
継承に関する話でよくある間違いとしては以下の様な物があると思います。
-
super.f()
って形で呼び出しちゃってる - メソッドを定義しているブロックの外で暗黙的に
super
を呼んじゃってる(後述) - そもそもよく見たら
Par
を継承してなかった
しかし実はこの使い方は端折った使い方であるということを述べていきたいと思います。多くの場合はこの書き方をそのまま覚えて、何が起きているのか分からないまま書いているのではないでしょうか。
次の章からはこの使い方を1回忘れて、基礎的な使い方から解説していこうと思います。
多重継承と__mro__の順番
super
の解説を行う前に、クリアにしなくてはいけないことがあります。もちろん継承です。
Pythonには多重継承やメタクラスといった概念が存在し、我々を苦しめます。この章では__mro__
を通して普通の継承と多重継承についてまとめていきます。
一番最初に結論だけ述べておきましょう。
Chi.__mro__
はChi
クラスから見たすべての親クラスを、参照する順番に格納しているタプル型オブジェクトである。
「親子関係の順番」ではなく「参照する順番」であることがポイントです。
それでは説明していきたいのですが、継承について触れる前にクラスとオブジェクトについて再確認しましょう。まず、Pythonではすべてがオブジェクトです。つまり、関数やクラスであっても変数に代入したり属性を呼び出したりできます。やってみましょう。
def f(): # 関数もまたオブジェクト
pass
g = f # 代入できる
print(g.__class__) # <class 'function'>
print(g.__name__) # f
print(g.__code__) # <code object f at 0x000001B527E3FCB0, file "d:\hogehoge.py", line 1>
代入はともかくとして、属性は色々ありますね(アンダーバーに囲まれた特殊属性しかありませんが)。g
に代入されたオブジェクトはfunction
クラスのオブジェクトで名前がf
でd:\hogehoge.py
の1行目で定義されたコードを持っている……といったことが見て取れます。
オブジェクトの属性はそのオブジェクトを説明するにほとんど十分な情報が入っていますが、idなどは見当たらないので属性からオブジェクトを逆算できるほど十分という訳ではではないのかもしれないですね。
今回は関数で試しましたが当然クラスもオブジェクトです。クラスの属性も見てみましょう。
class MyClass:
"""It is MyClass"""
pass
print(MyClass.__class__) # <class 'type'>
print(MyClass.__doc__) # It is MyClass
print(MyClass.__mro__) # (<class '__main__.MyClass'>, <class 'object'>)
MyClass
はtype
クラスのオブジェクトでDocstringがIt is MyClass
で__mro__
とやらがMyClass
とobject
だそうです。
type
クラスについては後で扱います。今大事なのは__mro__
です。
これはすべてのクラスが持つ属性で、そのクラスの親クラスの情報を持っています。
ただし、この記事で親クラスと言った時は自分自身も含むこととします。
少し話が逸れますが、自作クラスだけではなく他のクラスについても見てみましょう。
def f():
pass
function = f.__class__
print(int.__mro__) # (<class 'int'>, <class 'object'>)
print(str.__mro__) # (<class 'str'>, <class 'object'>)
print(bool.__mro__) # (<class 'bool'>, <class 'int'>, <class 'object'>)
print(function.__mro__) # (<class 'function'>, <class 'object'>)
print(type.__mro__) # (<class 'type'>, <class 'object'>)
どのクラスもobject
クラスを親クラスとして持っています。これがPythonはすべてがオブジェクトであると言われる理由の1つです。すべてはオブジェクトクラス(を継承しているクラス)のインスタンスなのだからオブジェクトクラスだ、という訳です。定義から明らかですね。
bool
がint
を継承しているのは妥当といえば妥当ですが、言われてみないと普段は意識できませんね。Pythonには他にもfloat
, complex
, list
, tuple
, set
, frozenset
, dict
などの組み込み型があります。これらはいずれもobject
クラスのみを継承するクラスになっています。つまらないですね。
def f():
pass
print(isinstance(0, object)) # True
print(isinstance("hoge", object)) # True
print(isinstance(f, object)) # True
print(isinstance(object, object)) # True
オブジェクトクラスすらオブジェクトクラスとなると数学的な定義のイメージで考えると定義の循環で矛盾が起きてしまいますね。
話を戻して__mro__
の挙動について詳細に見ていきます。__mro__
は親クラスをまとめているということでしたが、親クラスが複数ある場合はどうなるのでしょうか。
class Par:
pass
class Chi(Par):
pass
print(Chi.__mro__) # (<class '__main__.Chi'>, <class '__main__.Par'>, <class 'object'>)
このように、先のbool
の例でも見ましたが、子から親に向かって順々に記述されています。
さらに親クラスを増やしてみましょう。次のような継承関係を持つ場合です。
class Par:
pass
class Chi(Par):
pass
class GrandChi(Chi):
pass
print(GrandChi.__mro__) # (<class '__main__.GrandChi'>, <class '__main__.Chi'>, <class '__main__.Par'>, <class 'object'>)
予想通りの結果ですね。この感じだとChi.__mro__
はChi
クラスの親クラスを親子関係の順番に格納したタプル型オブジェクトであるように見えます。
つまり、__mro__
が(..., <class 'Hoge'>, <class 'Fuga'>, ...)
のようになっていた場合、常にFuga
クラスはHoge
クラスの親クラスである様に見えます。
しかしそれは誤りで、上述の様なケースでもFuga
クラスがHoge
クラスの親クラスではない場合があります。それはPythonが多重継承という概念を採用しているからです。
では多重継承と__mro__
について見ていきましょう。
多重継承とは簡単に言えば親がたくさんいる子どもで、例えば以下のようなケースを指します。
class Par0:
pass
class Par1:
pass
class Par2:
pass
class Chi(Par0, Par1, Par2): # 多重継承
pass
print(Chi.__mro__) # (<class '__main__.Chi'>, <class '__main__.Par0'>, <class '__main__.Par1'>, <class '__main__.Par2'>, <class 'object'>)
Chi
クラスがPar0
, Par1
, Par2
の3種のクラスを継承しています。この場合の__mro__
はこのように継承する際に記述した順がそのまま入ります。以降ではこれ(継承する際に記述した順)を継承順と呼ぶことにします(造語)。上記の例だと、Par0
はPar1
よりも継承順が高いと記述します。
もうちょっと複雑な例を見てみましょう。
class Par:
pass
class Chi0(Par):
pass
class Chi1(Par):
pass
class GrandChi(Chi0, Chi1):
pass
print(GrandChi.__mro__) # (<class '__main__.GrandChi'>, <class '__main__.Chi0'>, <class '__main__.Chi1'>, <class '__main__.Par'>, <class 'object'>)
どうやら__mro__
に追加される順番としては、感覚的には上の階層に行く前に同階層にあるクラスをすべて網羅すると解釈すれば問題無さそうです。
これに関する詳しい話は多重継承のひし形継承問題だとかダイアモンド継承だとかMethod Resolution Orderとかのキーワードで調べるとたくさん出てきます。Pythonに限った話をするなら、C3線形化アルゴリズムというアルゴリズムを使っているようです。
しかし感覚的な理解では少し不安ですし、だからといってアルゴリズムを一通り読んだだけでは分かったような分からないような感覚になってしまいます。ということで、次の章ではそのアルゴリズムを直感的に理解できる形で説明することに挑戦してみます。
__mro__の順番を直感的に理解しよう
このページ(The Python 2.3 Method Resolution Order)ではよりアルゴリズミックな解説があるので、このアルゴリズムを実装したいという場合や、この記事よりも公式ドキュメントの方が頼れるぜと思う場合はこちらを(/も)参照した方が良いと思います。この記事では挙動を理解することを最優先とします。
今更ですが、解説は以下のような図を元にして説明していきます。クラス名と継承関係しか記述されていないクラス図みたいなもんですね。子から親に向けて矢印が出ています。
また、矢印は継承順に沿って左から順に並ぶように描かれているものとします。
まずは次のような図で表される継承を考えます。先程見た継承です。
この時、GrandChi
クラスが自分からスタートしてこれら4つのクラスを仲間にしていくという状況を考えます。仲間にする際に以下のルールを設けます。
-
優先度ルール 仲間にするクラスの優先度は以下で定める。
- 上に移動する時は左側の矢印(継承順が高いクラス)を優先する。
- 古い仲間の親クラスよりも新しい仲間の親クラスを優先する。
-
制約ルール クラスを仲間にする際は以下を守る。
- 子クラスが全員仲間になっていない親クラスは仲間にできない。
- そのクラスよりも継承順が高いクラスが仲間になっていない場合は仲間にできない。
ルールは少しややこしいので、図を元に具体的な動きを見てみましょう。ボードゲームでコマを動かす感覚で理解すると分かりやすいと思います。
最初はGrandChi
一人ぼっちです。GrandChi
は親クラスとしてChi0
とChi1
の2クラスがあり、継承順はChi0
が先です。
そこで優先度ルールの1に従いChi0
を仲間に入れるとしましょう。Chi0
は子クラスがGrandChi
しかいないのでGrandChi
に誘われたらすぐに仲間になってくれました。
次に優先度ルールの2に従いChi0
の親であるPar
も誘ってみましょう。しかし制約ルールの1によってPar
はChi1
がハブにされているのが気に食わないようです。
そこで次はChi1
を仲間に誘いました。Chi1
も同じく子クラスがGrandChi
しかいないのですぐに仲間になってくれました。
最後にPar
です。Par
の子クラスは既に全員仲間なので、Par
も仲間に入ってくれました。
この時仲間に入ったクラスの順番(=GrandChi
, Chi0
, Chi1
, Par
)こそがGrandChi.__mro__
に格納されるクラスの順番になります。
別の例を見てみましょう。一番上のG
を仲間にすることが目標です。
まずA
はB
を仲間にし、その足でD
も仲間にします。B
はA
のみ、D
はB
のみを子クラスとするので、ここまでは順調ですね。
しかし、そのままG
に向かうとG
の子クラス(E
とF
)がいないので戻ります。さらにE
もE
の子クラス(C
)がいないという理由で仲間にできません。
そこで、C
を仲間にして、そのままE
も仲間にしました。それでもG
は仲間になりません。F
が残っているからです。
そして遂にF
が仲間になり、G
が仲間になります。
よってこの場合、A.__mro__
の順番はA
, B
, D
, C
, E
, F
, G
です。
このように__mro__
の順番は図さえイメージできれば、メモ帳や紙がなくても簡単に理解できるのです。
なお、このアルゴリズムは簡単な継承関係であれば順番付けが可能ですが、全てのクラスを仲間にする前にアルゴリズムが停止した場合に限り順番付けが不可能です。
何が言いたいかというと、C3線形化アルゴリズムというアルゴリズムはすべての継承関係に対応している訳では無いということです。
以下の例を見てください。
この例の場合に上のアルゴリズムを試してみてください。するとGrandChi
の次にChi
を仲間にしようとすると(Chi
よりも継承順が高いPar
がまだ仲間にできていないので)制約ルールの2に抵触し、Par
を仲間にしようとすると(Par
の子クラスであるChi
をまだ仲間にできていないので)制約ルールの1に抵触します。
このような場合はPythonのデフォルトのアルゴリズム(=C3線形化アルゴリズム)では扱えない継承関係なので、基本的にはどうしようもないです。
class Par:
pass
class Chi(Par):
pass
class GrandChi(Par, Chi): # TypeError: Cannot create a consistent method resolution order (MRO) for bases Par, Chi
pass
どうしてもPythonでも扱えない継承関係を扱いたい時や、C3線形化アルゴリズム以外のアルゴリズムを使いたいといった場合はmro
メソッドを変更することで実現できるらしいです。
すべてのクラスは__mro__
属性を持っていますが、これを解決しているのがmro
メソッドです。そしてこれを書き換えることでC3線形化アルゴリズムではない方法での継承が可能であるといった旨の言葉が公式のドキュメントに存在します。
その方法にはメタクラスを使用する必要があるので、詳しくはメタクラスに関する章で行います。
最後に、1つ用語を定義しておきます(造語です)。
上の例でD
の親クラスはどれでしょうか。もちろんG
です。
しかし、A.__mro__
においてD
の次のクラスはどれでしょうか。それはC
です。
これ(A.__mro__
におけるD
の次のクラス)を、(A
から見た)D
の(相対)親クラスと呼ぶこととします。括弧内は誤解の恐れがない限りは省略することもあります。
同様に、C
の相対親クラスはE
です。
superの基本
super
はクラスです。super
クラスです2。コンストラクタは2つの引数を持ち、例えばsuper(par, chi)
というふうに親子関係にあるクラスなどを入れることが出来ます。ここでもし第一引数par
と第二引数chi
の間に親子関係がない場合は以下の例外が発生します。
TypeError: super(type, obj): obj must be an instance or subtype of type
そうして得られるsuper
クラスのインスタンスはchi
から見たpar
の相対的親クラスと同じ挙動をします。
早速例を見ましょう。
class Par:
VAR = "Par"
class Chi(Par):
VAR = "Chi"
class GrandChi(Chi):
VAR = "GrandChi"
s = super(Chi, GrandChi)
print(s.VAR) # Par
確かにChi
の親クラスであるPar
のような挙動をしていますね。では相対的かどうかも確認しておきましょう。
class Par:
VAR = "Par"
class Chi0(Par):
VAR = "Chi0"
class Chi1(Par):
VAR = "Chi1"
class GrandChi(Chi0, Chi1):
VAR = "GrandChi"
s = super(Chi0, GrandChi)
print(s.VAR) # Chi1
ちゃんとChi0
の親クラスであるPar
ではなく、相対的親クラスであるChi1
の値が得られました。ではこの場合、s
のクラスを確認すればChi1
になって……
class Par:
VAR = "Par"
class Chi(Par):
VAR = "Chi"
class GrandChi(Chi):
VAR = "GrandChi"
s = super(Chi, GrandChi)
print(s.__class__) # <class 'super'>
ならないですね。さっきも言いましたがsuper(Chi, GrandChi)
はあくまでsuper
クラスのインスタンスです。
でもsuper
クラスにはVAR
属性なんて存在しないはずなのですが、さっきの例ではたしかに例外を発生させずにVAR
属性を呼び出すことができました。
脱線になりますが念の為super
オブジェクトの属性の一覧を取得して確認してみましょう。
Pythonにはオブジェクトの属性の一覧を取得する方法があります。それを使って属性を表示してみましょう。
ちなみにinspect
モジュールのgetmembers
関数はdir
関数を使って取得できるよりも多くの属性を取得できるらしいです(詳しくは知らない)。
from inspect import getmembers
class Par:
VAR = "Par"
class Chi(Par):
VAR = "Chi"
class GrandChi(Chi):
VAR = "GrandChi"
s = super(Chi, GrandChi)
print(list(dict(getmembers(s))))
# [
# '__class__',
# '__delattr__',
# '__dir__',
# '__doc__',
# '__eq__',
# '__format__',
# '__ge__',
# '__get__',
# '__getattribute__',
# '__gt__',
# '__hash__',
# '__init__',
# '__init_subclass__',
# '__le__',
# '__lt__',
# '__ne__',
# '__new__',
# '__reduce__',
# '__reduce_ex__',
# '__repr__',
# '__self__',
# '__self_class__',
# '__setattr__',
# '__sizeof__',
# '__str__',
# '__subclasshook__',
# '__thisclass__',
# ]
やはりsuper
クラスのオブジェクトの属性にVAR
はないみたいですね。ですが振る舞っているクラスに定義してある属性なら呼び出しても例外は出ないみたいです。
もし親クラスに定義してない属性であるhoge
を呼び出すとどうなるのでしょうか。以下のような例外が発生します。
AttributeError: 'super' object has no attribute 'hoge'
(そんなこと言ったらVARだってsuperクラスのオブジェクトには無い属性なんですけれどね……)
予想するに、super
インスタンス(s
)の特殊メソッドである__getattribute__
か__getattr__
が呼びだされたタイミングで相対的親クラスにそのような属性があるかどうか探しに行って、もし見つからなければs
自身の属性を確認している……というアルゴリズムのではないでしょうか……?(よく分からない)
実はsuper
はもう1つの使い方があります。というかむしろ通常はこっちを使うことを想定している気がします。
それはsuper
に2つの引数を渡すのは先程と同じなのですが、第二引数chi
が第一引数par
の子クラスのインスタンスである場合です。
先程と異なる点を挙げるなら、第二引数がクラスではなくてインスタンスであるということですね。
例を見てみましょう。
class Par:
VAR = "Par"
class Chi(Par):
VAR = "Chi"
chi = Chi()
s = super(Chi, chi)
print(s.VAR) # Par
動きましたね。別にさっきと変わらないというか、いわゆるシンタックスシュガーってやつでしょうか。
実はそうではなく、この場合は先程までとは少しだけ異なる挙動をします。親クラスのインスタンスメソッドが呼び出せるのです。つまり、こうです。
class Par:
def method(self):
print("Par")
class Chi(Par):
def method(self):
print("Chi")
chi = Chi()
s = super(Chi, chi)
s.method() # Par
先程までの(第二引数にインスタンスではなくクラスを渡す)方法では当然これは例外を吐いていました。クラスからインスタンスメソッドを呼び出した時に吐く例外と同じ例外です。
TypeError: Par.method() missing 1 required positional argument: 'self'
つまりsuper
は、第二引数にクラスを入れたら相対的親クラス、インスタンスを入れたら相対的親クラスのインスタンスみたいな振る舞いをしてくれるということですね。
superの明示的初期化/暗黙的初期化
ではそろそろsuper
の普段使っている使っている方法の話をしましょう。要はこれです。
引数なしでもOKなの?そうです、OKです。ここでは引数のない初期化の仕方をsuperの
暗黙的初期化と呼ぶことにします。逆に引数を書く場合を明示的初期化と呼びます(造語)。
引数を書かなくて良いのは楽ですが、その方法が使える場所には制限があります。
まず、class文のブロック外で引数なしでsuper
を呼び出すと以下の例外が発生します。
RuntimeError: super(): __class__ cell not found
また、class文のブロック内であっても、メソッド内に記述していないと以下の例外が発生します。
より正確には、super
が呼び出される行がmethod
クラスの属性を定義するdef文のブロック内でないと暗黙的な初期化はできません。
RuntimeError: super(): no arguments
あくまでメソッドでないとダメです。ゆえに、staticmethod
でデコレートされたメソッドはmethod
クラスではなくfunction
クラスになるので、RuntimeError: super(): no arguments
が発生してしまいます。
断言は出来ませんがsuper
の暗黙的初期化はシンタックスシュガーだと思います。つまり、暗黙的初期化を明示的初期化に書き換えられます。やってみましょう。
class Par:
def method(self):
print("Par@instance_method")
@classmethod
def cls_method(cls):
print("Par@class_method")
class Chi(Par):
def method(self):
s = super(Chi, self) # 明示的初期化!
s.cls_method() # クラスメソッドを呼び出してみる
s.method() # インスタンスメソッドを呼び出してみる
@classmethod
def cls_method(cls):
s = super(Chi, cls) # 明示的初期化!
s.cls_method() # クラスメソッドを呼び出してみる
s.method() # インスタンスメソッドを呼び出してみる
chi = Chi()
chi.method() # 問題なし
Chi.cls_method() # TypeError: Par.method() missing 1 required positional argument: 'self'
ちゃんと、クラスメソッドからインスタンスメソッドを呼び出す場合以外は動きますね。
メタクラスを使った応用
super
の主な使い方としては以上で終わりなのですが、さらに細かい挙動について記述するためにメタクラスというものを説明します。
2度目ですが、Pythonはすべてがオブジェクトです。先に見たように、クラスもオブジェクトです。つまり、何らかのクラスのインスタンスです。
ではクラスは何クラスのインスタンスなのでしょうか。正解はtype
クラスです。
おそらく多くのPython利用者はtype
と聞くと型を調べるための関数という印象があるんじゃないでしょうか。そう、これも関数じゃないんです。クラスです。
そしてこのクラスのインスタンスはクラスなのです。……分かりづらいですね。
図を使ってまとめてみましょう。
Pythonはすべてがオブジェクトです。
任意のオブジェクトx
は型(クラス)T
を持ちます。ここにおいて、x
とT
は「x
はT
を実現したもの」という関係を持ちます。この関係を今までの図に組み込んでみましょう。継承関係とは異なる関係なので、区別するために赤い矢印を使います。
そしてすべてのクラスT
もオブジェクトなので型を持ちます。そしてその型こそが型の親分type
なのです。
当然、type
もクラスでありオブジェクトです!したがって、type
の型はtype
です。
ちなみに無駄にややこしくなりますが、object
クラスも含めて継承関係も描くとこんな感じです。
object
クラスはtype
クラスのインスタンスであり、type
クラスはobject
クラスを継承しています。どちらが先に定義されたんでしょうか。卵と鶏は同時に生まれたんでしょうか。
type
がクラスであるということは、type(hogehoge)
と書けばtype
クラスのコンストラクタを呼び出して、新しくtype
クラスのインスタンス(=クラス)を生成できるということでしょうか。
そうです。その場合、必要な引数は3つ。__name
, __bases
, __dict
です。それぞれクラス名、継承したいクラス、属性を指定するのに使います。すなわち、以下の2つの例は同じクラスを定義しています。
class MyInt(int):
PI = 3
MyInt = type("MyInt", (int,), {"PI": 3})
このように実はclass文を使わなくてもクラスは定義できるんです。
もちろんメソッドも定義可能です。趣旨から外れるので詳しくは述べません。
このようにインスタンスがクラスであるようなクラスをメタクラスと言います。
では次に、type
を継承して新しいメタクラスを作ってみましょう。
メタクラスを定義すれば、クラスが定義される瞬間に何らかの処理を挟むことができます。プライベートな属性を標準出力するメタクラスとかどうでしょうか。
class MyType(type):
def __init__(self, __name: str, __bases: tuple[type], __dict: dict):
for name, value in __dict.items():
if name.startswith("_"):
print(value)
super().__init__(__name, __bases, __dict)
attributes = {"PI": 3, "__PI": 3.14}
MyClass = MyType("MyClass", (int,), attributes)
# 3.14
ちゃんと動きましたね。でもやっぱりいつものclass文が恋しいです。何が定義してあるのか見辛い……。
そこで、Pythonのclass文にはメタクラスを指定できる機能があります。使ってみましょう。
class MyType(type):
def __init__(self, __name: str, __bases: tuple[type], __dict: dict):
for name, value in __dict.items():
if name.startswith("_"):
print(value)
super().__init__(__name, __bases, __dict)
class MyClass(int, metaclass=MyType):
PI = 3
__PI = 3.14
# __main__
# MyClass
# 3.14
なんか余計なのが付いてきましたけど、とりあえず動きました。この余計なのが何なのかについては正直自分も分からないです。
class文はtype
で代替できるシンタックスシュガーだと思ってたんですけれど、もしかして違うんでしょうか。
最後にメタクラスを使って__mro__
を決定するアルゴリズムを変更するコードを紹介します。
メタクラスに定義したメソッドが、そのメタクラスを使って作られたクラスにどう影響を及ぼすのかについては分からない事が多いので自分としてもまだまだ調べる必要がありますね。
class MyType(type):
def mro(cls):
return [cls, object]
class Par:
pass
class Chi(Par, metaclass=MyType):
pass
print(Chi.__mro__) # (<class '__main__.Chi'>, <class 'object'>)
その他細かい状況
まずは親クラスのメソッドを暗黙的に継承している親クラスが存在する場合。例えば以下のような場合です。
class Par:
@classmethod
def f(cls):
print("Par")
class Chi0(Par):
pass # 暗黙的!
class Chi1(Par):
@classmethod
def f(cls):
print("Chi1")
class GrandChi(Chi0, Chi1):
@classmethod
def f(cls):
super().f()
GrandChi.f()
この場合、考えられるパターンとして以下の2通り存在します。
-
Chi0
がPar.f
を実行しようとするパターン。
この場合、Chi1.f
の処理は飛ばされるので出力としては"Par"
となる。 -
Chi0
がsuper().f
を実行しようとするパターン。
この場合、Chi1.f
が呼ばれるので出力としては"Chi1"
となる。
結論は後者になります。何も書かない場合は親クラスをsuper()
で呼び出しているようです。
まぁそうだと思ってましたが。
次に、メタクラスとそのインスタンスであるクラスが共通の親クラスを持っていて、さらに継承の関係が異なる形で、そのインスタンスから共通の親クラスをsuper
で呼び出す場合(?)。
言葉だと分かりづらいので図で表現するとこういう状況です。
class C(type):
VAR = "C"
class D(C):
VAR = "D"
class B(C):
pass
class A(B):
pass
a = A("a", (B, D), dict())
print(super(B, a).VAR)
この場合も考えられるパターンとして2通り存在します。
-
a
はA
のインスタンスなのだからA.__mro__
(この場合A
,B
,C
,type
,object
となる)を参照するという考え方で、出力としては"C"
になる。 -
a
自身がB
とD
を継承するクラスなのだから、a.__mro__
(この場合a
,B
,D
,C
,type
,object
となる)を参照するという考え方で、出力としては"D"
になる。
正解は後者です。ちなみに最後の1行をprint(super(B, A).VAR)
に変えると出力は"C"
になります。
おそらく、super(A, x)
はx.__mro__
からA
の有無を調べて、もしなければx.__class__.__mro__
からA
の有無を調べるというアルゴリズム……だと思います。(よく分からない)
最後に
super
ってどう考えても他のbuilt-inなクラスと比べてそれこそobject
やtype
にも引けを取らないくらい不思議な挙動をしているのに、どうしてここまで調べても記事の1つもないのだろうかと思い立ち、コードを書き書き色々試しました。
多分間違っている箇所が幾つかあるので、見つけたら教えて下さい。