第 100 次避免循环导入
Avoiding circular imports for the 100th time
总结
我在一个复杂的项目中一直有一个 ImportError
。我已经将它提炼到仍然会出现错误的最低限度。
例子
一个巫师有装有绿色和棕色药水的容器。这些可以加在一起,产生同样是绿色或棕色的新药水。
我们有一个 Potion
ABC,它从 PotionArithmatic
mixin 中获取 __add__
、__neg__
和 __mul__
。 Potion
有 2 个子 classes: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
的子class,无法更改 base.py
中 PotionArithmatic
的导入。
- 因为
GreenPotion
和BrownPotion
是Potion
的子class,所以在green.py
和[=48中导入Potion
=]无法更改。
- 那剩下
arithmatic.py
中的进口。这是必须进行更改的地方。
可能的解决方案
我花了好几个小时来研究这类问题。
通常的解决方案是不将classes Potion
、GreenPotion
和BrownPotion
导入文件arithmatic.py
,而是完整导入文件,并使用 base.Potion
、green.GreenPotion
、brown.BrownPotion
访问 classes。这我已经在上面的代码中完成了,并没有解决我的问题。
一个可能的解决方案是将导入移动到需要它们的函数中,如下所示:
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 方法,您可以想象这会产生许多额外的行,尤其是。如果这些依次调用模块顶层的函数。
- 任何其他解决方案...? 这确实有效并且不像上面代码块中的重复导入那样完全麻烦?
非常感谢!
你必须以某种方式打破 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
(Take 2)你能在你的 Mixin class' __init__
中做导入,将它们保存到属性中,然后从你的方法中引用它们吗?我认为这比在每个 method/function.
中导入东西更干净
./test.py
import potion
p1 = potion.Sub1()
p1.foo()
./potion/__init__.py
from .sub1 import Sub1
from .sub2 import Sub2
./potion/mixin.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
./potion/base.py
from .mixin import Mixin
class Base(Mixin):
pass
TLDR:经验法则
如果 mixin returns 是 class(或其后代之一)的实例,则不应在 mixin/inheritance 架构上使用。在这种情况下,方法应该附加到 class 对象本身。
详情:解决方案
我想到了 2 种(非常相似的)方法来让它工作。 None 是理想的,但它们似乎都解决了问题,不再依赖于 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,但手动附加方法
我最喜欢这个解决方案。在arithmatic.py
中,我们可以保留原来的PotionArithmatic
class。我们只是添加了一个相关的 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
class,直接将方法附加到 Potion
class 对象:
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 在编写代码时将不再知道这些方法,因为它们是在 run-time.
处添加的
总结
我在一个复杂的项目中一直有一个 ImportError
。我已经将它提炼到仍然会出现错误的最低限度。
例子
一个巫师有装有绿色和棕色药水的容器。这些可以加在一起,产生同样是绿色或棕色的新药水。
我们有一个 Potion
ABC,它从 PotionArithmatic
mixin 中获取 __add__
、__neg__
和 __mul__
。 Potion
有 2 个子 classes: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
是 mixinPotionArithmatic
的子class,无法更改base.py
中PotionArithmatic
的导入。 - 因为
GreenPotion
和BrownPotion
是Potion
的子class,所以在green.py
和[=48中导入Potion
=]无法更改。 - 那剩下
arithmatic.py
中的进口。这是必须进行更改的地方。
可能的解决方案
我花了好几个小时来研究这类问题。
通常的解决方案是不将classes
Potion
、GreenPotion
和BrownPotion
导入文件arithmatic.py
,而是完整导入文件,并使用base.Potion
、green.GreenPotion
、brown.BrownPotion
访问 classes。这我已经在上面的代码中完成了,并没有解决我的问题。一个可能的解决方案是将导入移动到需要它们的函数中,如下所示:
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 方法,您可以想象这会产生许多额外的行,尤其是。如果这些依次调用模块顶层的函数。
- 任何其他解决方案...? 这确实有效并且不像上面代码块中的重复导入那样完全麻烦?
非常感谢!
你必须以某种方式打破 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
(Take 2)你能在你的 Mixin class' __init__
中做导入,将它们保存到属性中,然后从你的方法中引用它们吗?我认为这比在每个 method/function.
./test.py
import potion
p1 = potion.Sub1()
p1.foo()
./potion/__init__.py
from .sub1 import Sub1
from .sub2 import Sub2
./potion/mixin.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
./potion/base.py
from .mixin import Mixin
class Base(Mixin):
pass
TLDR:经验法则
如果 mixin returns 是 class(或其后代之一)的实例,则不应在 mixin/inheritance 架构上使用。在这种情况下,方法应该附加到 class 对象本身。
详情:解决方案
我想到了 2 种(非常相似的)方法来让它工作。 None 是理想的,但它们似乎都解决了问题,不再依赖于 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,但手动附加方法
我最喜欢这个解决方案。在arithmatic.py
中,我们可以保留原来的PotionArithmatic
class。我们只是添加了一个相关的 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
class,直接将方法附加到 Potion
class 对象:
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 在编写代码时将不再知道这些方法,因为它们是在 run-time.
处添加的