[英]Avoiding circular imports for the 100th time
我一直在一个复杂的项目中遇到ImportError
。 我已经将它提炼到最低限度,仍然会给出错误。
巫师有装有绿色和棕色药水的容器。 这些可以加在一起,产生绿色或棕色的新药水。
我们有一个Potion
ABC,它的__add__
、 __neg__
和__mul__
来自PotionArithmatic
混合。 Potion
有 2 个子类: GreenPotion
和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
运行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.py
中PotionArithmatic
的导入。GreenPotion
和BrownPotion
是Potion
的子类,所以无法更改green.py
和brown.py
中Potion
的导入。arithmatic.py
中留下导入。 这是必须做出改变的地方。我已经研究了好几个小时来解决这类问题。
通常的解决方案是不将Potion
、 GreenPotion
和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 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
做什么取决于解决方案。
这个解决方案我最喜欢。 在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))
或者,我们可以完全摆脱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.