Python中的循环和嵌套导入
我现在遇到了一些麻烦,想弄清楚怎么正确地导入东西。我的应用结构是这样的:
main.py
util_functions.py
widgets/
- __init__.py
- chooser.py
- controller.py
我总是从根目录运行我的应用,所以大部分的导入看起来像这样:
from util_functions import *
from widgets.chooser import *
from widgets.controller import *
# ...
而我的 widgets/__init__.py
设置成这样:
from widgets.chooser import Chooser
from widgets.controller import MainPanel, Switch, Lever
__all__ = [
'Chooser', 'MainPanel', 'Switch', 'Lever',
]
一切都运行得很好,直到 widgets/controller.py
的代码变得有点长,我想把它拆分成多个文件:
main.py
util_functions.py
widgets/
- __init__.py
- chooser.py
- controller/
- __init__.py
- mainpanel.py
- switch.py
- lever.py
其中一个问题是 Switch
和 Lever
这两个类有静态成员,它们需要互相访问。使用 from ___ import ___
这种导入方式就造成了循环导入的问题。所以当我尝试运行重构后的应用时,导入的部分全都出错了。
我的问题是:我该怎么修复我的导入,以便能有这样一个不错的项目结构?我不能去掉 Switch
和 Lever
之间的静态依赖关系。
1 个回答
这个问题在官方的Python常见问题解答中有提到,具体可以查看如何处理相互导入的模块。
常见问题解答中明确指出,没有什么神奇的解决办法可以轻松解决这个问题。这里有一些选项(比常见问题解答中说得更详细):
- 尽量不要在模块的顶层放其他东西,除了类、函数和用常量或内置函数初始化的变量。不要使用
from spam import
这样的语句,这样通常就不会出现循环导入的问题。这种方法简单明了,但有时候你可能无法遵循这些规则。 - 重构模块,把导入语句放到模块中间的位置,这样每个模块在导入其他模块之前,先定义好需要导出的内容。这可能意味着把类分成两部分,一个“接口”类可以放在上面,另一个“实现”子类放在下面。
- 以类似的方式重构模块,但把“导出”代码(包括“接口”类)放到一个单独的模块中,而不是把它们放在导入语句上面。这样每个实现模块就可以导入所有的接口模块。这种方法和前一种效果相同,优点是代码更符合规范,更容易被人和自动工具理解,但缺点是模块数量会增加。
正如常见问题解答中提到的,“这些解决方案并不是互相排斥的。”特别是,你可以尽量把顶层代码移到函数内部,尽可能把from spam import …
这样的语句替换成import spam
,如果仍然有循环依赖,就通过重构,把不需要导入的导出代码放到上面或者放到一个单独的模块中来解决。
说完这些一般性的内容,我们来看看你的具体问题。
你的switch.Switch
和lever.Lever
类有“静态成员,每个类都需要访问另一个类”。我猜你的意思是它们有类属性,这些属性是通过另一个类的类属性或类方法初始化的?
按照第一个解决方案,你可以改变这些值的初始化时机,让它们在导入后进行初始化。假设你的代码是这样的:
class Lever:
switch_stuff = Switch.do_stuff()
# ...
你可以改成:
class Lever:
@classmethod
def init_class(cls):
cls.switch_stuff = Switch.do_stuff()
现在,在__init__.py
中,紧接着这个:
from lever import Lever
from switch import Switch
…你可以添加:
Lever.init_class()
Switch.init_class()
这就是关键:你通过明确初始化顺序来解决了模糊的初始化顺序问题。
另外,按照第二或第三个解决方案,你可以把Lever拆分成Lever
和LeverImpl
。然后你可以这样做(可以是两个单独的lever.py
和leverimpl.py
文件,或者一个文件中间放导入):
class Lever:
@classmethod
def get_switch_stuff(cls):
return cls.switch_stuff
from switch import Swift
class LeverImpl(Lever):
switch_stuff = Switch.do_stuff()
现在你不需要任何init_class
方法了。当然,你需要把属性改成方法——但如果你不喜欢这样,经过一些工作,你可以把它改成“类@property
”(可以通过写自定义描述符,或者在一个类中使用@property
)。
注意,你实际上不需要同时修复两个类来解决循环问题,只需要修复其中一个。理论上,修复两个类更干净,但在实际操作中,如果修复起来很麻烦,可能只需要修复那个相对简单的,另一个依赖关系就不管它了。