第100次避免循环进口



摘要

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

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

我们有一个PotionABC,它从PotionArithmatic混合中获得__add____neg____mul__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
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.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 类的方法,特别是如果这些方法反过来调用模块顶层的函数,这将产生许多额外的行。

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

非常感谢!

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

相关内容

  • 没有找到相关文章

最新更新