第 100 次避免循环导入

Avoiding circular imports for the 100th time

总结

我在一个复杂的项目中一直有一个 ImportError。我已经将它提炼到仍然会出现错误的最低限度。

例子

一个巫师有装有绿色和棕色药水的容器。这些可以加在一起,产生同样是绿色或棕色的新药水。

我们有一个 Potion ABC,它从 PotionArithmatic mixin 中获取 __add____neg____mul__Potion 有 2 个子 classes:GreenPotionBrownPotion.

在一个文件中,它看起来像这样:

onefile.py:

from abc import ABC, abstractmethod

def add_potion_instances(potion1, potion2): # some 'outsourced' arithmatic
    return BrownPotion(potion1.volume + potion2.volume)

class PotionArithmatic:
    def __add__(self, other):
        # Adding potions always returns a brown potion.
        if isinstance(other, base.Potion):
            return add_potion_instances(self, other)
        return BrownPotion(self.volume + other)

    def __mul__(self, other):
        # Multiplying a potion with a number scales it.
        if isinstance(other, Potion):
            raise TypeError("Cannot multiply Potions")
        return self.__class__(self.volume * other)

    def __neg__(self):
        # Negating a potion changes its color but not its volume.
        if isinstance(self, GreenPotion):
            return BrownPotion(self.volume)
        else:  # isinstance(self, BrownPotion):
            return GreenPotion(self.volume)

    # (... and many more)


class Potion(ABC, PotionArithmatic):
    def __init__(self, volume: float):
        self.volume = volume

    __repr__ = lambda self: f"{self.__class__.__name__} with volume of {self.volume} l."

    @property
    @abstractmethod
    def color(self) -> str:
        ...


class GreenPotion(Potion):
    color = "green"


class BrownPotion(Potion):
    color = "brown"


if __name__ == "__main__":

    b1 = GreenPotion(5)
    b2 = BrownPotion(111)

    b3 = b1 + b2
    assert b3.volume == 116
    assert type(b3) is BrownPotion

    b4 = b1 * 3
    assert b4.volume == 15
    assert type(b4) is GreenPotion

    b5 = b2 * 3
    assert b5.volume == 333
    assert type(b5) is BrownPotion

    b6 = -b1
    assert b6.volume == 5
    assert type(b6) is BrownPotion

这有效。

将文件拆分成可导入的模块

每个部分都放在文件夹 potions 内的自己的文件中,如下所示:

usage.py
potions
| arithmatic.py
| base.py
| green.py
| brown.py
| __init__.py

potions/arithmatic.py:

from . import base, brown, green

def add_potion_instances(potion1, potion2):
    return brown.BrownPotion(potion1.volume + potion2.volume)

class PotionArithmatic:
    def __add__(self, other):
        # Adding potions always returns a brown potion.
        if isinstance(other, base.Potion):
            return add_potion_instances(self, other)
        return brown.BrownPotion(self.volume + other)

    def __mul__(self, other):
        # Multiplying a potion with a number scales it.
        if isinstance(other, base.Potion):
            raise TypeError("Cannot multiply Potions")
        return self.__class__(self.volume * other)

    def __neg__(self):
        # Negating a potion changes its color but not its volume.
        if isinstance(self, green.GreenPotion):
            return brown.BrownPotion(self.volume)
        else:  # isinstance(self, BrownPotion):
            return green.GreenPotion(self.volume)

potions/base.py:

from abc import ABC, abstractmethod
from .arithmatic import PotionArithmatic

class Potion(ABC, PotionArithmatic):
    def __init__(self, volume: float):
        self.volume = volume

    __repr__ = lambda self: f"{self.__class__.__name__} with volume of {self.volume} l."

    @property
    @abstractmethod
    def color(self) -> str:
        ...

potions/green.py:

from .base import Potion

class GreenPotion(Potion):
    color = "green"

potions/brown.py:

from .base import Potion

class BrownPotion(Potion):
    color = "brown"

potions/__init__.py:

from .base import Potion
from .brown import GreenPotion
from .brown import BrownPotion

usage.py:

from potions import GreenPotion, BrownPotion

b1 = GreenPotion(5)
b2 = BrownPotion(111)

b3 = b1 + b2
assert b3.volume == 116
assert type(b3) is BrownPotion

b4 = b1 * 3
assert b4.volume == 15
assert type(b4) is GreenPotion

b5 = b2 * 3
assert b5.volume == 333
assert type(b5) is BrownPotion

b6 = -b1
assert b6.volume == 5
assert type(b6) is BrownPotion

运行 usage.py 给出以下 ImportError:

ImportError                               Traceback (most recent call last)
usage.py in <module>
----> 1 from potions import GreenPotion, BrownPotion
      2 
      3 b1 = GreenPotion(5)
      4 b2 = BrownPotion(111)
      5 

potions\__init__.py in <module>
----> 1 from .green import GreenPotion
      2 from .brown import BrownPotion

potions\brown.py in <module>
----> 1 from .base import Potion
      2 
      3 class GreenPotion(Potion):
      4     color = "green"

potions\base.py in <module>
      1 from abc import ABC, abstractmethod
      2 
----> 3 from .arithmatic import PotionArithmatic
      4  

potions\arithmatic.py in <module>
----> 1 from . import base, brown, green
      2 
      3 class PotionArithmatic:
      4     def __add__(self, other):

potions\green.py in <module>
----> 1 from .base import Potion
      2 
      3 class GreenPotion(Potion):
      4     color = "green"

ImportError: cannot import name 'Potion' from partially initialized module 'potions.base' (most likely due to a circular import) (potions\base.py)

进一步分析

可能的解决方案

我花了好几个小时来研究这类问题。

arithmatic.py:

def add_potion_instances(potion1, potion2):
    from . import base, brown, green # <-- added imports here
    return brown.BrownPotion(potion1.volume + potion2.volume)

class PotionArithmatic:
    def __add__(self, other):
        from . import base, brown, green # <-- added imports here
        # Adding potions always returns a brown potion.
        if isinstance(other, base.Potion):
            return add_potion_instances(self, other)
        return brown.BrownPotion(self.volume + other)

    def __mul__(self, other):
        from . import base, brown, green # <-- added imports here
        # Multiplying a potion with a number scales it.
        if isinstance(other, base.Potion):
            raise TypeError("Cannot multiply Potions")
        return self.__class__(self.volume * other)

    def __neg__(self):
        from . import base, brown, green # <-- added imports here
        # Negating a potion changes its color but not its volume.
        if isinstance(self, green.GreenPotion):
            return brown.BrownPotion(self.volume)
        else:  # isinstance(self, BrownPotion):
            return green.GreenPotion(self.volume)

虽然这行得通,但如果文件包含更多 mixin class 方法,您可以想象这会产生许多额外的行,尤其是。如果这些依次调用模块顶层的函数。

非常感谢!

你必须以某种方式打破 class 依赖关系的循环。 我还没有尝试过,但我认为以下策略可能会奏效。 这个想法是首先构建 class PotionArithmatic 没有依赖关系。然后你可以在 class 完全构建后注入方法。但这可能与您的解决方案一样麻烦:

class PotionArithmatic:
    external_add = None
    external_mul = None
    external_neg = None
    def __add__(self, other):
        return PotionArithmatic.external_add(self,other)

    def __mul__(self, other):
        return PotionArithmatic.external_mul(self,other)

    def __neg__(self):
        return PotionArithmatic.external_neg(self)

在外部文件中,您需要:

def external_add(a,b):
    pass # put your code here

def external_mul(a,b):
    pass # put your code here

def external_neg(a):
    pass # put your code here

PotionArithmatic.external_add = external_add
PotionArithmatic.external_mul = external_mul
PotionArithmatic.external_neg = external_neg

(Take 2)你能在你的 Mixin class' __init__ 中做导入,将它们保存到属性中,然后从你的方法中引用它们吗?我认为这比在每个 method/function.

中导入东西更干净

./test.py

import potion

p1 = potion.Sub1()
p1.foo()

./potion/__init__.py

from .sub1 import Sub1
from .sub2 import Sub2

./potion/mixin.py

def bar(p):
    return isinstance(p, p.sub1.Sub1) or isinstance(p, p.sub2.Sub2)

class Mixin:
    def __init__(self):
        from . import sub1
        from . import sub2
        self.sub1 = sub1
        self.sub2 = sub2    
    
    def foo(self):
        return bar(self)

    def baz(self):
        return self.sub1.Sub1(), self.sub2.Sub2()

./potion/sub1.py

from .base import Base

class Sub1(Base):
    pass

./potion/sub2.py

from .base import Base

class Sub2(Base):
    pass

./potion/base.py

from .mixin import Mixin

class Base(Mixin):
    pass

TLDR:经验法则

如果 mixin returns 是 class(或其后代之一)的实例,则不应在 mixin/inheritance 架构上使用。在这种情况下,方法应该附加到 class 对象本身。

详情:解决方案

我想到了 2 种(非常相似的)方法来让它工作。 None 是理想的,但它们似乎都解决了问题,不再依赖于 mixin 的继承。

在两者中,potions/base.py 文件更改为以下内容:

potions/base.py:

from abc import ABC, abstractmethod

class Potion(ABC): # <-- mixin is gone
    # (nothing changed here)

from . import arithmatic  # <-- moved to the end
arithmatic.append_methods()  # <-- explicitly 'do the thing'

我们用 potions/arithmatic.py 做什么取决于解决方案。

保留 mixin class,但手动附加方法

我最喜欢这个解决方案。在arithmatic.py中,我们可以保留原来的PotionArithmaticclass。我们只是添加了一个相关的 dunder 方法列表,以及 append_methods() 函数来进行追加。

potions/arithmatic.py:

from . import base, brown, green

def add_potion_instances(potion1, potion2):
    # (nothing changed here)

def PotionArithmatic:
    ATTRIBUTES = ["__add__", "__mul__", "__neg__"] # <-- this is new
    # (nothing else changed here)

def append_methods(): # <-- this is new as well
    for attr in PotionArithmatic.ATTRIBUTES:
        setattr(base.Potion, attr, getattr(PotionArithmatic, attr))

彻底摆脱mixin

或者,我们可以完全去掉 PotionArithmatic class,直接将方法附加到 Potion class 对象:

potions/arithmatic.py:

from . import base, brown, green

def _add_potion_instances(potion1, potion2):
    return brown.BrownPotion(potion1.volume + potion2.volume)

def _ext_add(self, other):
    # Adding potions always returns a brown potion.
    if isinstance(other, base.Potion):
        return _add_potion_instances(self, other)
    return brown.BrownPotion(self.volume + other)

def _ext_mul(self, other):
    # Multiplying a potion with a number scales it.
    if isinstance(other, base.Potion):
        raise TypeError("Cannot multiply Potions")
    return self.__class__(self.volume * other)

def _ext_neg(self):
    # Negating a potion changes its color but not its volume.
    if isinstance(self, green.GreenPotion):
        return brown.BrownPotion(self.volume)
    else:  # isinstance(self, BrownPotion):
        return green.GreenPotion(self.volume)

def append_methods():
    base.Potion.__add__ = _ext_add
    base.Potion.__mul__ = _ext_mul
    base.Potion.__neg__ = _ext_neg

后果

两种解决方案都有效,但请注意

(a) 他们引入了更多耦合并需要将导入移动到 base.py 的末尾,并且

(b) IDE 在编写代码时将不再知道这些方法,因为它们是在 run-time.

处添加的