Python

Python モジュールをまたいだクラスの型について

更新日:2022-12-26 公開日:2022-12-26

今回は自作クラスを型として使う場合の小ネタです。
例えばある親クラスがあって、子クラスを腹持ちしているような場合の型付けを想定しています。

今回の例

適当な親となるAnimalクラスと子となるCatクラスとDogクラスを用意してみました。

class Animal:

    def __init__(self, name: str):
        self.name = name
        self.dog = Dog(self)
        self.cat = Cat(self)

    def make_greeting(self, meld: str):
        print(f'Hi,My Name is {self.name} {meld}!')


class Cat:
    def __init__(self, parent: Animal):
        self.parent = parent

    def greeting(self):
        self.parent.make_greeting('meow meow')


class Dog:
    def __init__(self, parent: Animal):
        self.parent = parent

    def greeting(self):
        self.parent.make_greeting('bow bow')


通常は親を継承する形で子クラスを定義する場合が多いのかもしれません。
このプログラムはこういう使われ方を想定しています。

animal = Animal('Taro')
animal.dog.greeting()
animal.cat.greeting()
>>>
Hi,My Name is Taro bow bow!
Hi,My Name is Taro meow meow!


親となるAnimalクラスをインスタンス化し、そこから子のメソッドを呼ぶ形です。

前置きが長くなりましたが、今回は型の話がメインでした。

class Cat:
    def __init__(self, parent: Animal):
        self.parent = parent


ここの部分に注目してみると、parentに作成したAnimalクラスの型をあてています。
同一モジュール ならこのやり方で型の恩恵を受けられそうですが、子クラスを別モジュールに分離したらどうでしょうか。

モジュールの切り分け

Animalクラスはanimal.py,CatクラスとDogクラスをchild.pyに切り分けます。

それぞれ以下のようになります。

# animal.py
from child import Cat, Dog


class Animal:

    def __init__(self, name: str):
        self.name = name
        self.dog = Dog(self)
        self.cat = Cat(self)

    def make_greeting(self, meld: str):
        print(f'Hi,My Name is {self.name} {meld}!')


# child.py
from animal import Animal


class Cat:
    def __init__(self, parent: Animal):
        self.parent = parent

    def greeting(self):
        self.parent.make_greeting('meow meow')


class Dog:
    def __init__(self, parent: Animal):
        self.parent = parent

    def greeting(self):
        self.parent.make_greeting('bow bow')


一見問題なさそうですが、実行するとImportErrorになります。

ImportError: cannot import name 'Cat' from partially initialized module 'child' (most likely due to a circular import)


要はお互いにimportしてるので循環になっているわけです。
ナイーブな解決策としてはchild.pyでAnimalクラスのimportを行わないことです。
しかしそうすると、開発時に型の恩恵を受けることができなくなってしまいます。

解決策

ファイルは切り分けたい、型の恩恵も受けたい、というときにはtyping.TYPE_CHECKINGを使うと解決ができます。

typing.TYPE_CHECKING
サードパーティーの静的型検査器が True と仮定する特別な定数です。 実行時には False になります。
https://docs.python.org/ja/3/library/typing.html


こういう風に使います。

# child.py
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from animal import Animal


class Cat:
    def __init__(self, parent: "Animal"):
        self.parent = parent

    def greeting(self):
        self.parent.make_greeting('meow meow')


class Dog:
    def __init__(self, parent: "Animal"):
        self.parent = parent

    def greeting(self):
        self.parent.make_greeting('bow bow')


型の指定にはクラス名を文字列で指定します。



開発時に型があたっていることが分かります。
実行時は常にFalseとなるのでインポートが行われません。つまり、ImportErrorは起きません。

animal = Animal('Taro')
animal.dog.greeting()
animal.cat.greeting()
>>>
Hi,My Name is Taro bow bow!
Hi,My Name is Taro meow meow!


おわりに

子のクラスの数が多く、モジュールを切り分けたいというケースは往々にしてあると思います。
モジュールを切り分ける且つ型の恩恵を受けたいときの引き出しの一つとしてtyping.TYPE_CHECKINGを持っておくとよいでしょう。
今回のAnimalの例えは微妙だったかもしれませんが、例えばAPI ClientのSDKでこういう構成がとられていることがあります。

Notion SDKの例

https://github.com/ramnes/notion-sdk-py