今回は自作クラスを型として使う場合の小ネタです。
例えばある親クラスがあって、子クラスを腹持ちしているような場合の型付けを想定しています。
適当な親となる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 になります。
こういう風に使います。
# 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でこういう構成がとられていることがあります。