3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonのsuperについて調べた(mroとメタクラス)

Posted at

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クラスのオブジェクトで名前がfd:\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'>)

MyClasstypeクラスのオブジェクトでDocstringがIt is MyClass__mro__とやらがMyClassobjectだそうです。
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つです。すべてはオブジェクトクラス(を継承しているクラス)のインスタンスなのだからオブジェクトクラスだ、という訳です。定義から明らかですね。
boolintを継承しているのは妥当といえば妥当ですが、言われてみないと普段は意識できませんね。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の例でも見ましたが、子から親に向かって順々に記述されています。
さらに親クラスを増やしてみましょう。次のような継承関係を持つ場合です。

pu1.png

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__について見ていきましょう。
多重継承とは簡単に言えば親がたくさんいる子どもで、例えば以下のようなケースを指します。

pu2.png

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__はこのように継承する際に記述した順がそのまま入ります。以降ではこれ(継承する際に記述した順)を継承順と呼ぶことにします(造語)。上記の例だと、Par0Par1よりも継承順が高いと記述します。
もうちょっと複雑な例を見てみましょう。

pu3.png

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)ではよりアルゴリズミックな解説があるので、このアルゴリズムを実装したいという場合や、この記事よりも公式ドキュメントの方が頼れるぜと思う場合はこちらを(/も)参照した方が良いと思います。この記事では挙動を理解することを最優先とします。
今更ですが、解説は以下のような図を元にして説明していきます。クラス名と継承関係しか記述されていないクラス図みたいなもんですね。子から親に向けて矢印が出ています。

pu1.png

また、矢印は継承順に沿って左から順に並ぶように描かれているものとします。

pu2.png

まずは次のような図で表される継承を考えます。先程見た継承です。

pu3.png

この時、GrandChiクラスが自分からスタートしてこれら4つのクラスを仲間にしていくという状況を考えます。仲間にする際に以下のルールを設けます。

  • 優先度ルール 仲間にするクラスの優先度は以下で定める。
    1. 上に移動する時は左側の矢印(継承順が高いクラス)を優先する。
    2. 古い仲間の親クラスよりも新しい仲間の親クラスを優先する。
  • 制約ルール クラスを仲間にする際は以下を守る。
    1. 子クラスが全員仲間になっていない親クラスは仲間にできない。
    2. そのクラスよりも継承順が高いクラスが仲間になっていない場合は仲間にできない。

ルールは少しややこしいので、図を元に具体的な動きを見てみましょう。ボードゲームでコマを動かす感覚で理解すると分かりやすいと思います。

最初はGrandChi一人ぼっちです。GrandChiは親クラスとしてChi0Chi1の2クラスがあり、継承順はChi0が先です。
そこで優先度ルールの1に従いChi0を仲間に入れるとしましょう。Chi0は子クラスがGrandChiしかいないのでGrandChiに誘われたらすぐに仲間になってくれました。

pu3-1.png

次に優先度ルールの2に従いChi0の親であるParも誘ってみましょう。しかし制約ルールの1によってParChi1がハブにされているのが気に食わないようです。

pu3-2.png

そこで次はChi1を仲間に誘いました。Chi1も同じく子クラスがGrandChiしかいないのですぐに仲間になってくれました。

pu3-3.png

最後にParです。Parの子クラスは既に全員仲間なので、Parも仲間に入ってくれました。

pu3-4.png

この時仲間に入ったクラスの順番(=GrandChi, Chi0, Chi1, Par)こそがGrandChi.__mro__に格納されるクラスの順番になります。

別の例を見てみましょう。一番上のGを仲間にすることが目標です。

pu4.png

まずABを仲間にし、その足でDも仲間にします。BAのみ、DBのみを子クラスとするので、ここまでは順調ですね。

pu4-1.png

しかし、そのままGに向かうとGの子クラス(EF)がいないので戻ります。さらにEEの子クラス(C)がいないという理由で仲間にできません。

pu4-2.png

そこで、Cを仲間にして、そのままEも仲間にしました。それでもGは仲間になりません。Fが残っているからです。

pu4-3.png

そして遂にFが仲間になり、Gが仲間になります。

pu4-4.png

よってこの場合、A.__mro__の順番はA, B, D, C, E, F, Gです。

このように__mro__の順番は図さえイメージできれば、メモ帳や紙がなくても簡単に理解できるのです。

なお、このアルゴリズムは簡単な継承関係であれば順番付けが可能ですが、全てのクラスを仲間にする前にアルゴリズムが停止した場合に限り順番付けが不可能です。
何が言いたいかというと、C3線形化アルゴリズムというアルゴリズムはすべての継承関係に対応している訳では無いということです。
以下の例を見てください。

pu5.png

この例の場合に上のアルゴリズムを試してみてください。すると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を持ちます。ここにおいて、xTは「xTを実現したもの」という関係を持ちます。この関係を今までの図に組み込んでみましょう。継承関係とは異なる関係なので、区別するために赤い矢印を使います。

pu7.png

そしてすべてのクラスTもオブジェクトなので型を持ちます。そしてその型こそが型の親分typeなのです。

pu8.png

当然、typeもクラスでありオブジェクトです!したがって、typeの型はtypeです。

pu9.png

ちなみに無駄にややこしくなりますが、objectクラスも含めて継承関係も描くとこんな感じです。

pu10.png

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通り存在します。

  • Chi0Par.fを実行しようとするパターン。
    この場合、Chi1.fの処理は飛ばされるので出力としては"Par"となる。
  • Chi0super().fを実行しようとするパターン。
    この場合、Chi1.fが呼ばれるので出力としては"Chi1"となる。

結論は後者になります。何も書かない場合は親クラスをsuper()で呼び出しているようです。
まぁそうだと思ってましたが。

次に、メタクラスとそのインスタンスであるクラスが共通の親クラスを持っていて、さらに継承の関係が異なる形で、そのインスタンスから共通の親クラスをsuperで呼び出す場合(?)。
言葉だと分かりづらいので図で表現するとこういう状況です。

pu11.png

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通り存在します。

  • aAのインスタンスなのだからA.__mro__(この場合A, B, C, type, objectとなる)を参照するという考え方で、出力としては"C"になる。
  • a自身がBDを継承するクラスなのだから、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なクラスと比べてそれこそobjecttypeにも引けを取らないくらい不思議な挙動をしているのに、どうしてここまで調べても記事の1つもないのだろうかと思い立ち、コードを書き書き色々試しました。
多分間違っている箇所が幾つかあるので、見つけたら教えて下さい。

  1. 昔良かれと思ってSupSub(SuperClassとSubClassより)を使っていたら、「似ていて分かりづらい!」と言われました。たしかに。

  2. Pythonに限った話じゃないかもしれませんが、関数に見えてクラスだったみたいなの多いですよね。実はmappropertyも関数じゃなくてクラスですしね。callableなオブジェクトと関数を区別するものはfunctionクラスか否かくらいだと思ってます。すなわち命名と認識からくる扱いの問題で挙動は同じ。ですよね?

3
0
0

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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?