摘要
我一直在一个复杂的项目中ImportError
。我已经将其提炼到仍然给出错误的最低限度。
例
巫师有装有绿色和棕色药水的容器。这些可以加在一起,产生新的药水,也是绿色或棕色的。
我们有一个Potion
ABC,它从PotionArithmatic
混合中获得__add__
,__neg__
和__mul__
。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
potionsbrown.py in <module>
----> 1 from .base import Potion
2
3 class GreenPotion(Potion):
4 color = "green"
potionsbase.py in <module>
1 from abc import ABC, abstractmethod
2
----> 3 from .arithmatic import PotionArithmatic
4
potionsarithmatic.py in <module>
----> 1 from . import base, brown, green
2
3 class PotionArithmatic:
4 def __add__(self, other):
potionsgreen.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) (potionsbase.py)
进一步分析
- 因为
Potion
是mixinPotionArithmatic
的一个子类,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 类的方法,特别是如果这些方法反过来调用模块顶层的函数,这将产生许多额外的行。
- 还有其他解决方案吗?这实际上有效并且不像上面代码块中的重复导入那样完全繁琐?
非常感谢!
TLDR:经验法则
如果 mixin 返回类(或其后代之一)的实例,则不应在 mixin/继承体系结构上使用。在这种情况下,应将方法追加到类对象本身。
详细信息:解决方案
我想到了 2 种(非常相似)让它工作的方法。没有一个是理想的,但它们似乎都解决了这个问题,不再依赖继承来混合。
在这两种情况下,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))
完全摆脱混合蛋白
或者,我们可以完全摆脱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
年底,以及
(b) IDE 在编写代码时将不再知道这些方法,因为它们是在运行时添加的。
你必须以某种方式打破类依赖的循环。 我还没有尝试过,但我认为以下策略可能会奏效。 这个想法是首先构造没有依赖关系的PotionArithmatic类。然后,您可以在完全构造类后注入方法。但它可能与您的解决方案一样麻烦:
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
(取 2) 您能否在 Mixin 类的__init__
中进行导入,将它们保存到属性中,然后从您的方法中引用它们?我认为这比在每个方法/函数中导入东西更干净。
./test.py
import potion
p1 = potion.Sub1()
p1.foo()
./药水/__init__.py
from .sub1 import Sub1
from .sub2 import Sub2
./药水/混合.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
./药水/碱.py
from .mixin import Mixin
class Base(Mixin):
pass