ElR*_*udi 8 python inheritance circular-dependency
TLDR:当基类在可导入模块中返回子类实例时,如何避免循环导入错误?
我从其他位置/问题收集了一些解决方案(参见下面的 AD),但恕我直言,没有一个是令人满意的。
# onefile.py
from abc import ABC, abstractmethod
class Animal(ABC):
def __new__(cls, weight: float):
if cls is Animal:
# Try to return subclass instance instead.
for subcls in [Dog, Cat]:
try:
return subcls(weight)
except ValueError:
pass
raise NotImplementedError("No appropriate subclass found.")
return super().__new__(cls)
@property
@abstractmethod
def weight(self) -> float:
"""weight of the animal in kg."""
...
class Dog(Animal):
def __init__(self, weight: float = 5):
if not (1 < weight < 90):
raise ValueError("No dog has this weight")
self._weight = weight
weight: float = property(lambda self: self._weight)
class Cat(Animal):
def __init__(self, weight: float = 5):
if not (0.5 < weight < 15):
raise ValueError("No cat has this weight")
self._weight = weight
weight: float = property(lambda self: self._weight)
if __name__ == "__main__":
a1 = Dog(34)
try:
a2 = Dog(0.9) # ValueError
except ValueError:
pass
else:
raise RuntimeError("Should have raised Exception!")
a3 = Cat(0.8)
try:
a4 = Cat(25) # ValueError
except ValueError:
pass
else:
raise RuntimeError("Should have raised Exception!")
a5 = Animal(80) # can only be dog; should return dog.
assert type(a5) is Dog
a6 = Animal(0.7) # can only be cat; should return cat.
assert type(a6) is Cat
a7 = Animal(10) # can be both; should return dog.
assert type(a7) is Dog
try:
a8 = Animal(400)
except NotImplementedError:
pass
else:
raise RuntimeError("Should have raised Exception!")
Run Code Online (Sandbox Code Playgroud)
该文件运行正确。
我想要Cat
,Dog
和Animal
作为模块中的可导入类zoo
。为此,我创建了一个文件夹zoo
,其中包含文件animal.py
、dog.py
、cat.py
和__init__.py
。该文件usage.py
保存在父文件夹中。这些文件如下所示:
# zoo/animal.py
from abc import ABC, abstractmethod
from .dog import Dog
from .cat import Cat
class Animal(ABC):
def __new__(cls, *args, **kwargs):
if cls is Animal:
# Try to return subclass instance instead.
for subcls in [Dog, Cat]:
try:
return subcls(*args, **kwargs)
except ValueError:
pass
raise NotImplementedError("No appropriate subclass found.")
return super().__new__(cls)
@property
@abstractmethod
def weight(self) -> float:
"""weight of the animal in kg."""
...
# zoo/dog.py
from .animal import Animal
class Dog(Animal):
def __init__(self, weight: float = 5):
if not (1 < weight < 90):
raise ValueError("No dog has this weight")
self._weight = weight
weight: float = property(lambda self: self._weight)
# zoo/cat.py
from .animal import Animal
class Cat(Animal):
def __init__(self, weight: float = 5):
if not (0.5 < weight < 15):
raise ValueError("No cat has this weight")
self._weight = weight
weight: float = property(lambda self: self._weight)
# zoo/__init__.py
from .dog import Dog
from .cat import Cat
from .animal import Animal
# usage.py
from zoo import Dog, Cat, Animal
a1 = Dog(34)
try:
a2 = Dog(0.9) # ValueError
except ValueError:
pass
else:
raise RuntimeError("Should have raised Exception!")
a3 = Cat(0.8)
try:
a4 = Cat(25) # ValueError
except ValueError:
pass
else:
raise RuntimeError("Should have raised Exception!")
a5 = Animal(80) # can only be dog; should return dog.
assert type(a5) is Dog
a6 = Animal(0.7) # can only be cat; should return cat.
assert type(a6) is Cat
a7 = Animal(10) # can be both; should return dog.
assert type(a7) is Dog
try:
a8 = Animal(400)
except NotImplementedError:
pass
else:
raise RuntimeError("Should have raised Exception!")
Run Code Online (Sandbox Code Playgroud)
这是目前不起作用的;重构重新引入了ImportError (...) (most likely due to a circular import)
. 问题在于animal.py
引用dog.py
和cat.py
,反之亦然。
有一些可能性(一些取自链接的问题);这里有一些选项。代码示例仅显示文件的相关部分如何更改。
Animal
类定义之后from abc import ABC, abstractmethod
class Animal(ABC):
def __new__(cls, *args, **kwargs):
if cls is Animal:
# Try to return subclass instance instead.
for subcls in [dog.Dog, cat.Cat]: # <-- instead of [Dog, Cat]
try:
return subcls(*args, **kwargs)
except ValueError:
pass
raise NotImplementedError("No appropriate subclass found.")
return super().__new__(cls)
(...)
from . import dog # <-- import module instead of class, and import at end, to avoid circular import error
from . import cat # <-- same
Run Code Online (Sandbox Code Playgroud)
这有效。
缺点:
dog.py
,这个Dog
类确实是只需要的。令人困惑的是它是完全导入的(尽管这被一些人认为是最佳实践)。# zoo/animal.py
from abc import ABC, abstractmethod
class Animal(ABC):
def __new__(cls, *args, **kwargs):
from .dog import Dog # <-- imports here instead of at module level
from .cat import Cat # <-- imports here instead of at module level
if cls is Animal:
# Try to return subclass instance instead.
for subcls in [Dog, Cat]:
try:
return subcls(*args, **kwargs)
except ValueError:
pass
raise NotImplementedError("No appropriate subclass found.")
return super().__new__(cls)
(...)
Run Code Online (Sandbox Code Playgroud)
这也有效。
缺点:
Dog
或Cat
需要,则需要重复导入。# zoo/animal.py
from abc import ABC, abstractmethod
class Animal(ABC):
def __new__(cls, *args, **kwargs):
if cls is Animal:
# Try to return subclass instance instead.
subclasses = {sc.__name__: sc for sc in Animal.__subclasses__()} # <-- create dictionary
for subcls in [subclasses["Dog"], subclasses["Cat"]]: # <-- instead of [Dog, Cat]
try:
return subcls(*args, **kwargs)
except ValueError:
pass
raise NotImplementedError("No appropriate subclass found.")
return super().__new__(cls)
(...)
Run Code Online (Sandbox Code Playgroud)
这也有效。为了避免每次都创建字典,也可以使用此答案中所示的注册表。
缺点:
# zoo/animal.py
from abc import ABC, abstractmethod
_Dog = _Cat = None # <-- dummies, to be assigned by subclasses.
class Animal(ABC):
def __new__(cls, *args, **kwargs):
if cls is Animal:
# Try to return subclass instance instead.
for subcls in [_Dog, _Cat]: # <-- instead of [Dog, Cat]
try:
return subcls(*args, **kwargs)
except ValueError:
pass
raise NotImplementedError("No appropriate subclass found.")
return super().__new__(cls)
(...)
# zoo/dog.py
from . import animal
from .animal import Animal
class Dog(Animal):
(...)
animal._Dog = Dog # <-- update protected variable
# zoo/cat.py analogously
Run Code Online (Sandbox Code Playgroud)
这也有效。
缺点:
_Dog
和_Cat
中的变量zoo/animal.py
代表什么。在我看来,AD 都不令人满意,我想知道是否还有其他方法。 这就是你进来的地方。 ;) 可能没有其他方法 - 在这种情况下,我很想知道你首选的方法是什么,以及为什么。
非常感谢
恕我直言,您只需要一个简单的包,并在文件中进行适当的初始化__init__.py
:
整体结构:
zoo folder accessible from the Python path
| __init__.py
| animal.py
| dog.py
| cat.py
| other files...
Run Code Online (Sandbox Code Playgroud)
Animal.py - 不直接依赖于任何其他模块
from abc import ABC, abstractmethod
subclasses = [] # will be initialized from the package __init__ file
class Animal(ABC):
def __new__(cls, *args, **kwargs):
if cls is Animal:
# Try to return subclass instance instead.
for subcls in subclasses:
try:
return subcls(*args, **kwargs)
except ValueError:
pass
raise NotImplementedError("No appropriate subclass found.")
return super().__new__(cls)
@property
@abstractmethod
def weight(self) -> float:
"""weight of the animal in kg."""
...
Run Code Online (Sandbox Code Playgroud)
dog.py - 取决于动物
from .animal import Animal
class Dog(Animal):
def __init__(self, weight: float = 5):
if not (1 < weight < 90):
raise ValueError("No dog has this weight")
self._weight = weight
weight: float = property(lambda self: self._weight)
Run Code Online (Sandbox Code Playgroud)
cat.py:id。狗
init .py:导入所需的子模块并初始化animal.subclasses
from .animal import Animal
from .dog import Dog
from .cat import Cat
from . import animal as _animal # the initial _ makes the variable protected
_animal.subclasses = [Dog, Cat]
Run Code Online (Sandbox Code Playgroud)
从那时起,记录的接口仅包含包zoo
本身及其类Animal
和Dog
。Cat
可以这样使用:
from zoo import Animal, Dog, Cat
if __name__ == "__main__":
a1 = Dog(34)
try:
a2 = Dog(0.9) # ValueError
except ValueError:
pass
else:
raise RuntimeError("Should have raised Exception!")
a3 = Cat(0.8)
try:
a4 = Cat(25) # ValueError
except ValueError:
pass
else:
raise RuntimeError("Should have raised Exception!")
a5 = Animal(80) # can only be dog; should return dog.
assert type(a5) is Dog
a6 = Animal(0.7) # can only be cat; should return cat.
assert type(a6) is Cat
a7 = Animal(10) # can be both; should return dog.
assert type(a7) is Dog
try:
a8 = Animal(400)
except NotImplementedError:
pass
else:
raise RuntimeError("Should have raised Exception!")
Run Code Online (Sandbox Code Playgroud)
这种结构允许简单的直接依赖。它甚至可以改进为允许可选子类可以通过在中声明(或导入)的特定函数添加__init__.py