読者です 読者をやめる 読者になる 読者になる

ゆくゆくは有へと

おかゆ/オカ∃/大鹿有生/彼ノ∅有生 の雑記

それっぽい図を書くことで分かった気になるぜ(__init_subclass__)

Python3.6出ましたね!

主要なものとしては

あたりでしょうか。上2つは割とそのままで、3つ目はそもそもasync関連を勉強してからじゃないとなので、4つ目が現状の知識で一番楽しいかなという感じ。

atsuoishimoto.hatenablog.com

PEP 487 -- Simpler customisation of class creation | Python.org

わかった気になれる図

f:id:waraby0ginger:20161227061812p:plain

あまり真に受けないでください

というわけで(ひとつのユースケース

UMLのクラス図の矢印の方向でも察せる通り、継承関係を知ってるのはサブクラス側だけで、スーパークラスは誰が自分を継承してるのかということは知りません。

なので、下々のクラスどもを一括して管理したいようなとき、1つの方法としては、特定のメタクラスで生成してあげて、そのメタクラス側で生成したクラスのことを覚えておく…というのがあります。メタクラスは自分が誰を生成したのかということは知ってますから、別にどうってことないですよね。(というのが分かった気になれる図の左側)

でもそういうことするためにわざわざメタクラス学ばないといけないというのも学習障壁がしんどい。

というわけで、Python3.6では、めちゃくちゃ大雑把に言って、スーパークラスが「誰が自分を継承したか」ということを知れるようになりました。これによって、誰かが自分を継承したときに何か反応を起こすことができるようになります(__init_subclass__)し、まさに下々のクラスどもを一括して管理できるようにもなります。

というのは半分嘘で(ええ・・・)、もう少し真面目に言うと、__init_subclass__はクラスメソッドであり、このメソッドを実装しているスーパークラスを継承する際に、サブクラスがこのメソッドを呼び出します。呼び出すと言っても、そのクラスメソッドは(オーバーライドしない限り)スーパークラスのものですから、「継承時にスーパークラス__init_subclass__メソッドを呼び出す」ことには変わりありません。

スーパークラスがサブクラスを弄るという説明は(見た目的にはそれに近いことが起こりますが)微妙で、まあ単に親クラスのクラスメソッドの呼び出しです。

まあ、自分が持ってないクラス変数なら、親を探索するのはどんなメソッドでも同じですから、キモは「継承時呼び出し」というところですね。

PEP-487 の例を参考に(『Effective Python』項目34はまさにこれのメタクラスバージョンです):

class PluginBase:
    subclasses = []

    def __init_subclass__(cls, xxx, **kwargs):
        super().__init_subclass__(**kwargs)
        print(cls)
        cls.subclasses.append(cls)
        cls.xxx = xxx


class C(PluginBase, xxx=10):
    ...


class D(PluginBase, xxx=20):
    ...

クラスCとDは__init_subclass__を実装しているPluginBaseのサブクラスであり、これらがPluginBaseを継承するタイミングで、このメソッドが(サブクラスから)呼ばれます(なので、clsに入るのはサブクラスであるCやDです)。

PluginBase は subclasses リストをクラス変数として持っており、そこに呼び出したクラス cls を追加していくわけですね(cls.subclasses.append(cls)に10分くらい戸惑っていましたが、cls.subclassesスーパークラス(PluginBase)の変数を参照しようとしてるわけですね。でもこれ、C, Dがクラス定義でsubclasses変数持ってたらそっちにアクセスされちゃうのでダメです。なので、__class__.subclassesでアクセスするのがより安全だとは思います)。

で、今まで親クラスやメタクラスなど書いてたところに、__init_subclass__が引数として取る値を入れれるようになりました。上の例ではxxxがそれです。指定外のキーワード引数は kwds に吸収されるっぽい。

これが何に使えるのか…というところですが、メタクラスの代理としてスーパークラスを用いるためとしてこの機能があることを踏まえると、今までのクラス生成時におけるクラス変数の調整のところを代替できるようになるんじゃないですかね(しらんけど)。

もうちょいまともに見てみる

メタクラスがそれに従うクラスを生成するときに引数としてとるのは

  • クラス名
  • 親クラスのタプル
  • そのクラスの属性辞書

の3つです。ところで、__init_subclass__ においては、この3つは

  • クラス名:cls.__name__
  • 親クラスのタプル:cls.__mro__[1:]
  • 属性辞書:cls.__dict__

でアクセス可能ですから、実際、メタクラス__new__でできたことの大抵は__init_subclass__でできるようになったんじゃないかと思います。

『Effective Python』の項目33, 34, 35 のようなことは__init_subclass__(と__set_name__)で実現できるようになったと思います。

たとえば、具象クラスのチェックが抽象クラスに書けるようになったのはなかなかお得な感じがします。Effective Python 33のソースコードを書き換えてみました:

class Polygon:
    sides = None

    @classmethod
    def interior_angles(cls):
        return (cls.sides - 2) * 180

    def __init_subclass__(cls, sides, **kwds):
        if sides < 3:
            raise ValueError("Polygons need 3+ sides")
        cls.sides = sides


class Triangle(Polygon, sides=3):
    pass


class FalsePolygon(Polygon, sides=2):
    pass

Polygon を基底クラスとして、これを継承するサブクラスの sides は3以上であることをサブクラス生成/継承時にチェックします。

sides をわざわざ引数として渡さずに、クラス定義の中に書いてしまってもいいとは思いますが、まあチェック時に値を属性辞書から取り出すようにするかどうかの違いしかないですね。

当該PEPによれば、__init_subclass__メタクラス__new__内部で実行されるようなので、ここで例外があがればクラス自体生成されないことになるようです(この辺りはインスタンス生成の__init__と挙動が少し異なりますね(インスタンスの場合、__init__が失敗してもインスタンス自体は作られてしまうので))。

この例を見ると、抽象基底クラス(ABC)とかなり相性がよさそうに見えますね!今までは具象クラスで実装してほしいクラス変数は、ABCではとりあえず None にしておいて書いておくという方法しかなかったですが、__init_subclass__ の引数として必須にしておけば、具象化のためにその値が必要だということが分かる上に、姑息にNoneを入れて変数の存在を示唆する必要もなくなりますので、お役立ちって感じです。

あと(ケースとして必要かは微妙ですが)、サブクラスはその属性をもっていてほしいが、スーパークラスには持たせたくない、かつ、だけどサブクラスでその属性の値を指定しない場合はデフォルト値を想定したい、というようなときは、デフォルト引数としてその値を渡せばいいですよね。

おわり

メタクラスのメタな操作がスーパークラスまで降りてきたことで、メタいことが学習障壁を低くして実現できるようになったってだけの話ではありますが、ABCがその具象化に対する責任を持てるようになったというのは結構偉大なことかとは思います。

とはいえ、単純にメタいことをスーパークラスに書いちゃえるということは、それだけ概念的に混乱するリスクも増えるということでしょうし、その辺りは注意しないといけないかもしれません。

まあメタメタ言ってますが、スーパークラスのサブクラスへの干渉という観点でみれば、そんなメタメタしくはない気もします。見方によりますね。上の方で半分嘘と言いながら説明した「スーパークラスが誰から継承を受けたか知れるように(そしてある程度操作できるように)なった」という観点が増えたと思えばいいかもしれない。学習障壁云々の話をしましたが、実際じゃあこのカスタムメソッドを説明するときに「今まではメタクラスでやってたことがね」という導入はメタクラスの概念を持ちこんでしまってるわけであまりよろしくはないと思います。メタクラスで実現していたという事実はあったとしても、あくまでクラスレベルでの関係性としてこのカスタムメソッドを捉えて説明しないことには本格的な学習障壁の低下にはならんでしょう。そうなると、「スーパーからサブ方向への接触を可能にする」という形になるんじゃないかと思います。

話かわって。THEメタといえば、動的なクラス生成がありますが、これもそんな凝ったものでなければtypeと__init_subclass__だけで十分まかなえそうだし、クラスレベルでかなりやれることが増えたなという感じはします!

ま、メタクラスを使わない方法でいえば、今までもクラスデコレータがありましたけどね!それに加えてもう一つ新しい方法が増えたというくらいに考えるのがいいかも。