繁体   English   中英

第 100 次避免循环进口

[英]Avoiding circular imports for the 100th time

概括

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

例子

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

我们有一个Potion ABC,它的__add____neg____mul__来自PotionArithmatic混合。 Potion有 2 个子类: 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)

进一步的分析

  • 因为Potion是 mixin PotionArithmatic的子类,所以不能更改base.pyPotionArithmatic的导入。
  • 因为GreenPotionBrownPotionPotion的子类,所以无法更改green.pybrown.pyPotion的导入。
  • 这将在arithmatic.py中留下导入。 这是必须做出改变的地方。

可能的解决方案

我已经研究了好几个小时来解决这类问题。

  • 通常的解决方案是不将PotionGreenPotionBrownPotion类导入到文件arithmatic.py中,而是将文件完整地导入,并使用base.Potiongreen.GreenPotionbrown.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 class 的方法,您可以想象这会导致许多额外的行,尤其是。 如果这些反过来调用模块顶层的函数。

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

非常感谢!

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

这个解决方案我最喜欢。 arithmatic.py中,我们可以保留原来的PotionArithmatic class。 我们只需添加一个相关的dunder方法列表就可以了, append_methods() function 来做追加。

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,而只是 append 直接使用Potion classB2668CFDE6831ACBD4 的方法:

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

两者都引入了更多的耦合,并且需要将导入移动到base.py的末尾,但除此之外 - 它们可以工作。

您必须以某种方式打破 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

我做了一些测试,发现了以下作品。 对于您的代码,这意味着将 brown.py 和 green.py 组合成一个模块(colors.py?),但它仍然允许您在不遇到导入错误的情况下拆分内容。

./test.py

import potion

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

./potion/__init__.py

from .sub import Sub1, Sub2

./药水/mixin.py

from . import sub

class Mixin:
    def foo(self):
        return sub.Sub1(), sub.Sub2()

./potion/sub.py

from .base import Base

class Sub1(Base):
    pass

class Sub2(Base):
    pass

./药水/base.py

from .mixin import Mixin

class Base(Mixin):
    pass

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM