はじめに
オーイシおにーさん、ありがとうううううう!! vol.2おめでとおおおおおお!! nikkieです。
自作のSphinx拡張を例に、Pythonにおける継承について書いていきます。
やってはいけない事象の共有になります(📣俺みたいになるな!)
目次
- はじめに
- 目次
- 自作Sphinx拡張、当初の実装
- Pythonの組み込み関数 super()
- 魔が差した:自作Translatorのメソッドを、他のTranslatorに代入できるのでは?
- super(self.__class__, self)で無限の再帰が発生するのは、なぜ?
- 解消方法(ただしハックなので持ち帰らないでください)
- 終わりに
自作Sphinx拡張、当初の実装
ドキュメンテーションツールSphinxの拡張を公開しています1。
有効にすると、ビルドしたHTMLにある外部サイトに遷移するリンクが、ブラウザの別のタブで開くようになります。
実装を手短に言うと、HTMLを出力するTranslatorを以下に差し替えています2。
# https://github.com/ftnext/sphinx-new-tab-link/blob/v0.2.1/sphinx_new_tab_link/__init__.py class NewTabLinkHTMLTranslator(HTMLTranslator): def starttag(self, node, tagname, *args, **atts): # 外部サイトのリンクを別のタブで開くようにする処理 return super().starttag(node, tagname, *args, **atts)
Pythonの組み込み関数 super()
https://docs.python.org/ja/3/library/functions.html#super
スーパークラスのメソッドを呼び出すために使います。
MRO(method resolution order)3により、親クラスまたは兄弟クラスのメソッドに解決されます。
公式ドキュメントの例はこちら:
class C(B): def method(self, arg): super().method(arg)
super().method(arg)
はC
の親クラスB
のmethod()
を呼び出します。
これはsuper(C, self).method(arg)
と同じです(コード中のコメントより)。
魔が差した:自作Translatorのメソッドを、他のTranslatorに代入できるのでは?
その時思い出していた例
『ゼロから作るDeep Learning ❸』に以下のような実装があります。
# https://github.com/oreilly-japan/deep-learning-from-scratch-3/blob/af431eaa918283820ecbfe0bf8a9cdf49fc972ab/steps/step20.py#L161-L162
Variable.__add__ = add
Variable.__mul__ = mul
特殊メソッドに関数を代入しています4。
これをTranslatorでもできるのではないか、と思ったのです。
継承してもいいんですが、代入するほうがかっこいい(超☆主観)し、使うのは自分だけだからやっちゃえ(※魔が差しています)
アイデア実現のための試行錯誤
普通に代入して
class MyTranslator(HTML5Translator): ... MyTranslator.starttag = NewTabLinkHTMLTranslator.starttag
MyTranslator
をTranslatorに指定すると、うまくいきません。
TypeError: super(type, obj): obj must be an instance or subtype of type
タイトル(h1)の出力にあたり
self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
が呼び出されます。
MyTranslatorの(代入で用意した)starttag()
メソッドが呼ばれるわけですが、その中の
return super().starttag(node, tagname, *args, **atts)
のsuper()
で上記のTypeErrorです。
これはMyTranslator
のstarttag()
メソッドとして、NewTabLinkHTMLTranslator
のstarttag()
メソッドを代入していることによるという理解です。
- ここの
super()
はsuper(NewTabLinkHTMLTranslator, self)
と同じstarttag()
メソッドはNewTabLinkHTMLTranslator
にある
- 実行時に
self
はMyTranslator
NewTabLinkHTMLTranslator
とself
のMyTranslator
には継承関係はないNewTabLinkHTMLTranslator
のインスタンスでも、サブタイプでもないため、上記のエラー
ここから更にハックしました(※罪を重ねました)
# https://github.com/ftnext/sphinx-new-tab-link/blob/v0.2.2/sphinx_new_tab_link/__init__.py class NewTabLinkHTMLTranslator(HTMLTranslator): def starttag(self, node, tagname, *args, **atts): # 外部サイトのリンクを別のタブで開くようにする処理 return super(self.__class__, self).starttag( node, tagname, *args, **atts )
🤔MyTranslator
はTranslatorを継承しているので、self.__class__
と指定したら、親のTranslatorのstarttagメソッド5を呼ぶのではないか
https://docs.python.org/ja/3/library/stdtypes.html#instance.__class__
クラスインスタンスが属しているクラスです。
これでメソッド代入のケースは動作するようになります。
顛末:報告された無限の再帰
super(self.__class__, self)
と書いているために無限の再帰エラーが報告されました。
pydata-sphinx-theme は実行時に動的に継承したTranslatorクラスを生成します。
現在は修正済みです。
super(self.__class__, self)
で無限の再帰が発生するのは、なぜ?
StackOverflowにわかりやすい解説がありました。
B
のインスタンスは作れます
>>> # python -i bad_use_super.py でクラス定義を読み込んでから対話モードに入っています >>> B(1, 2) <class '__main__.B'> <__main__.B object at 0x102723fd0>
>>> C(2, 3) <class '__main__.C'> <class '__main__.C'> RecursionError: maximum recursion depth exceeded while calling a Python object <class '__main__.C'>
この仕組みについての解説(の私の理解)
C().__init__()
は、親クラスB
に定義された__init__()
を呼び出す- その中で
self.__class__
はC
super(C, self).__init__(v, v2)
、これは親クラスB
の__init__()
の呼び出し
- その中で
self.__class__
はC
であることは変わらないので、同じ呼び出しの繰り返しRecursionError
送出
解消方法(ただしハックなので持ち帰らないでください)
__init__()
の例で解消
これを解消するには、self.__class__
を使わないのが一番です。
ただ上記のメソッド代入の都合上、self.__class__
は今の私には必要だったので、残そうとすると以下のコードとなりました。
class B(A): def __init__(self, v, v2): print(self.__class__) - super(self.__class__, self).__init__(v, v2) + if self.__class__ is B: + super(self.__class__, self).__init__(v, v2) + else: + super(B, self).__init__(v, v2)
>>> B(1, 2) <class '__main__.B'> <__main__.B object at 0x1024abfd0> >>> C(2, 3) <class '__main__.C'> <__main__.C object at 0x1024abb80>
無限の再帰は発生しなくなりました。
C().__init__()
でB
の__init__()
が呼び出されたとき、self.__class__
はC
なのでelse側が実行され、無限再帰となりません。
これは無理やりself.__class__
を残しています
Translatorに近い例で解消
終わりに
自作Sphinx拡張を例に、super(self.__class__, self)
って書いたらメソッド代入できるようになったけど、代償として無限の再帰の修正が必要になったことを書きました。
真似しないでくださいね。
メソッド代入でやろうとしたのが筋がすこぶる悪かったと認識しています。
Mixinを定義し、それを動的に継承してTranslatorクラスを作る実装をpydata-sphinx-themeに見つけている6ので、それをパクってメソッド代入自体を今後不要にしていきます!