[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