第 100 次避免循环导入

2024-05-06

Summary

我继续有一个ImportError在一个复杂的项目中。我已经将其蒸馏到仍然会出现错误的最低限度。

Example

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

我们有一个PotionABC,得到它的__add__, __neg__ and __mul__来自PotionArithmatic mixin. Potion有2个子类:GreenPotion and BrownPotion.

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

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

Running 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)

更深入的分析

  • Because Potion是 mixin 的子类PotionArithmatic,进口PotionArithmatic in base.py无法更改。
  • Because GreenPotion and BrownPotion是的子类Potion,进口Potion in green.py and brown.py无法更改。
  • 这使得进口arithmatic.py。这是必须做出改变的地方。

可能的解决方案

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

  • 通常的解决方案是不导入类Potion, GreenPotion, and BrownPotion进入文件arithmatic.py,而是完整导入文件,并使用以下命令访问类base.Potion, green.GreenPotion, brown.BrownPotion。这我已经在上面的代码中完成了,并没有解决我的问题。

  • 一个可能的解决方案是将导入移至需要它们的函数中,如下所示:

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 类的更多方法,尤其是,这会导致许多额外的行。如果这些依次调用模块顶层的函数。

  • 还有其他解决方案吗...?这确实有效,并且并不像上面代码块中的重复导入那样完全麻烦?

非常感谢!


TLDR:经验法则

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

详细信息:解决方案

我想到了两种(非常相似)的方法来让它发挥作用。没有一个是理想的,但它们似乎都解决了问题,不再依赖 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 类,但手动附加方法

我最喜欢这个解决方案。在arithmatic.py,我们可以保留原来的PotionArithmatic班级。我们只需添加相关 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类全部放在一起,只需将方法直接附加到Potion类对象:

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, and

(b) IDE 在编写代码时将不再知道这些方法,因为它们是在运行时添加的。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

第 100 次避免循环导入 的相关文章

随机推荐