[Python] Import cyclesを解消する
はじめに
テスト駆動開発のjavaで書かれた部分をpythonで実装しているとき、
ImportError: cannot import name 'Money' from partially initialized module 'moneys.money' (most likely due to a circular import)
というエラーに遭遇しました。 これは循環参照と呼ばれるものであり、
# money.py from __future__ import annotations from .expression import Expression class Money(Expression): ... def plus(self, added: Money) -> Expression: from .total import Total return Total(self, added) ...
# expression.py from __future__ import annotations from abc import ABCMeta, abstractmethod from .money import Money class Expression(metaclass=ABCMeta): @abstractmethod def reduce(self, to: str) -> Money: pass
というような形で、money.py
でExpressionをインポートしているのに、expression.py
ではMoneyをインポートするという循環参照と呼ばれる状態に陥っているため生じているエラーです。
このエラーはfrom typing import TYPE_CHECKING
を用いて解消できます。
解消法
# expression.py from __future__ import annotations from abc import ABCMeta, abstractmethod # 追加 if TYPE_CHECKING: from .money import Money class Expression(metaclass=ABCMeta): @abstractmethod def reduce(self, to: str) -> Money: pass
とするだけでエラーは解消されます。
より詳しく
どうしてこの2行のコードで解消されるのかをみていきます。
string literal types
pythonの関数で型を指定するとき、その型をstring型で書くことができます。
例えば、下のコードではdef f(a: A) -> None: ...
と引数aの型をA
としてしまうと、pythonはまだclass A
を読み込んでいないことからエラーを出します。
しかし、引数の型をstring型の'A'
としてやるとエラーを解消できます。
def f(a: 'A') -> None: ... class A: pass
annotations
同様のことがfrom __future__ import annotations
により可能となります。
以下のコードでは関数fの引数はstring型ではありませんが、annotationsをインポートしていることでエラーなしで実行できます。これは自動で型アノテーションをstring literal化するようなものだとドキュメントに書かれています。
from __future__ import annotations def f(a: A) -> None: ... class A: pass
typing.TYPE_CHECKING
typingモジュールはTYPE_CHECKINGという変数を定義しており、この変数はランタイム中にはTYPE_CHECKING=False
であり、型チェック中にはTYPE_CHECKING=True
となります。
このことから、if TYPE_CHECKING:
内に循環参照の原因となるモジュールをインポートするとランタイム中には実行されないため、エラーを起こさず実行することができます。
したがって、解消法で示したように循環参照のもととなっているモジュールのインポートをif TYPE_CHECKING:
内に書くことでエラーが解消できることがわかります。
最後に
参考にしたドキュメントは以下になります。 mypy.readthedocs.io
こちらはTDDの本のコードをpythonで実装したときのレポジトリです。 github.com