带继承的包中的循环导入依赖

4 投票
1 回答
1161 浏览
提问于 2025-04-17 08:44

我在我的项目里大致有这样的结构:

thing.py:

from otherthing import *

class Thing(Base):
    def action(self):
        ...do something with Otherthing()...

subthing.py:

from thing import *

class Subthing(Thing):
    pass

otherthing.py:

from subthing import *

class Otherthing(Base):
    def action(self):
        ... do something with Subthing()...

如果我把所有的内容放到一个文件里,它是可以工作的,但那个文件会变得非常大,这样维护起来就会很困难。我该怎么解决这个问题呢?

1 个回答

8

这段内容讨论的是在Python中常见的循环导入问题,不过我认为即使设计得很好,有时候还是需要循环引用。

那么,可以试试以下方法:

thing.py:

class Thing(Base):
    def action(self):
        ...do something with otherthing.Otherthing()...

import otherthing

subthing.py:

import thing

class Subthing(thing.Thing):
    pass

otherthing.py:

class Otherthing(Base):
    def action(self):
        ... do something with subthing.Subthing()...

import subthing

这里有几个要点。首先,先了解一下背景。

在Python中,导入模块的方式会导致一个正在被导入的模块(但还没有完全解析完)在其他模块的导入语句中被认为已经导入了。所以,如果你在其他模块中引用这个模块,可能会得到一个还在解析中的符号。如果解析还没到你需要的符号,就会找不到它,进而抛出异常。

解决这个问题的一种方法是使用“尾部导入”。这个技巧的目的是在可能触发其他模块导入之前,先定义好其他模块可能需要的符号。

另一种处理循环引用的方法是将基于from的导入改为普通的import。这样有什么好处呢?当你使用from风格的导入时,目标模块会被导入,然后在那一刻就会查找from语句中引用的符号。

而使用普通的import语句时,查找引用会被延迟,直到某个地方真正访问这个模块的属性。通常可以将这个查找推迟到一个函数或方法中,这样在所有导入完成之前,这个函数或方法通常不会被执行。

不过,这两种方法在类层次结构中有循环引用时就不太管用了。因为导入必须在子类定义之前完成,而表示超类的属性在class语句执行时必须存在。最好的办法是使用普通的import,通过模块引用超类,并希望能调整其他代码使其正常工作。

如果你还是卡在这里,另一种可以帮助你的技巧是使用访问器函数来调解一个模块和另一个模块之间的访问。例如,如果你在一个模块中有类A,想从另一个模块引用它,但由于循环引用无法做到,你可以创建一个第三个模块,里面有一个函数返回类A的引用。如果你把这个方法推广成一系列的访问器函数,这就不再是听起来那么复杂的黑科技了。

如果这些方法都不行,你可以把导入语句放到你的函数和方法里面,但我通常把这个作为最后的手段。

--- 编辑 ---

我最近发现了一些新东西。在class语句中,超类实际上是一个Python表达式。所以,你可以这样做:

>>> b=lambda :object
>>> class A(b()):
...     pass
... 
>>> a=A()
>>> a
<__main__.A object at 0x1fbdad0>
>>> a.__class__.__mro__
(<class '__main__.A'>, <type 'object'>)
>>> 

这样可以让你定义并导入一个访问器函数,从而在另一个类定义中访问某个类。

撰写回答