如何避免Python中的循环导入?

201

我知道Python中循环导入的问题已经被多次提及过,我也阅读了这些讨论。在这些讨论中,反复强调的是循环导入是设计不良的标志,应该重新组织代码以避免循环导入。

有人能告诉我如何避免在这种情况下出现循环导入吗?:我有两个类,并且我希望每个类都有一个构造函数(方法),它接受另一个类的实例并返回类的实例。

更具体地说,一个类是可变的,一个类是不可变的。需要不可变类进行哈希、比较等操作。同时也需要可变类来完成一些任务。这类似于集合和frozenset或列表和元组。

我可以将两个类的定义放在同一个模块中。还有其他建议吗?

一个玩具示例是A类具有列表属性,B类具有元组属性。然后,类A具有一个方法,它接受类B的实例并返回类A的实例(通过将元组转换为列表),类B也类似地具有一个方法,它接受类A的实例并返回类B的实例(通过将列表转换为元组)。


你尝试过使用abc模块来解决这个问题吗?请参考https://docs.python.org/3/library/abc.html。 - undefined
3个回答

369

考虑以下示例Python包,其中a.pyb.py彼此依赖:

/package
    __init__.py
    a.py
    b.py

循环导入问题的类型

循环导入依赖通常分为两类,取决于您正在尝试导入什么以及在每个模块中使用它的位置。 (以及您是否正在使用Python 2还是3)。

1. 导入具有循环依赖性的模块时出现错误

在某些情况下,即使您没有引用导入的模块中的任何内容,仅仅导入一个具有循环导入依赖关系的模块也可能会导致错误。

Python有几种标准的导入模块方式。

import package.a           # (1) Absolute import
import package.a as a_mod  # (2) Absolute import bound to different name
from package import a      # (3) Alternate absolute import
import a                   # (4) Implicit relative import (deprecated, python 2 only)
from . import a            # (5) Explicit relative import

很遗憾,只有第一种和第四种选项在存在循环依赖时才能正常工作(其余都会引发ImportErrorAttributeError)。一般情况下,你不应该使用第四种语法,因为它仅适用于Python 2,并有与其他第三方模块冲突的风险。所以,只有第一种语法是保证能够工作的。

编辑:在Python 3中,ImportErrorAttributeError问题仅出现在Python 2中。在Python 3中,导入机制已被重写,所有这些导入语句(除第4种外)都可以正常工作,即使存在循环依赖。虽然本节中的解决方案可能有助于重构Python 3代码,但主要是为那些使用Python 2的人准备的。

绝对导入

只需使用上面提到的第一种导入语法。这种方法的缺点是对于大型包来说,导入名称可能会变得非常长。

a.py中:

import package.b
b.py 中。
import package.a

延迟导入直至后续需要

我看到很多包都使用了这种方法,但它仍然感觉有点取巧,我不喜欢无法在模块顶部看到所有依赖项的情况,而必须搜索所有函数。

a.py

def func():
    from package import b
b.py中。
def func():
    from package import a

将所有import放在一个中心模块中

这种方法也可以,但与第一种方法存在同样的问题,即所有包和子模块的调用都变得非常冗长。 它还有两个主要缺点 - 它强制导入所有子模块,即使您只使用了一两个,而且您仍然无法查看任何子模块并快速查看其顶部的依赖项,必须浏览函数。

__init__.py

from . import a
from . import b

a.py

import package

def func():
    package.b.some_object()

b.py

import package

def func():
    package.a.some_object()

2. 使用循环依赖导入对象时出错

在某些情况下,您可能能够导入具有循环依赖关系的模块,但是您将无法导入该模块中定义的任何对象, 或在导入模块的模块的顶级任何地方引用该导入的模块。但是,您可以在不会在导入时运行的函数和代码块中使用导入的模块。

例如,以下内容将起作用:

package/a.py

import package.b

def func_a():
    return "a"

包/package.b.py

import package.a

def func_b():
    # Notice how package.a is only referenced *inside* a function
    # and not the top level of the module.
    return package.a.func_a() + "b"

但这行不通。

package/a.py

import package.b

class A(object):
    pass

包/package/b.py

import package.a

# package.a is referenced at the top level of the module
class B(package.a.A):
    pass

您将会得到一个异常

属性错误:模块'package'没有属性'a'

通常,在大多数有效的循环依赖情况下,可以重构或重新组织代码以防止这些错误并将模块引用移动到代码块内部。


17
Brendan,这是一个非常详尽的答案!我读了一百个有关解决循环导入的答案,终于明白了。谢谢!顺便说一句,你可能想要在你的列表中再添加一种解决方案:在文件顶部为每个包设置全局变量为None,然后在运行时将模块注入全局变量。这种方法的优点是所有模块名称都在文件顶部。 - Dan Oblinger
2
将导入延迟到函数内部,如果该函数被重复调用,是否会变得更慢?或者导入只会发生一次? - Hassan Baig
6
我对"this has been fixed in Python 3."这句话很好奇,你有没有一个好的参考资料描述这个变化是怎么发生的?我知道在Python 2和3之间在那个领域发生了变化,但是在Python 2.7.11和Python 3.5.1上,通过from...import的一个小例子仍然以相同的方式失败。 - Rob Hague
1
@RobHague 在你的例子中,你试图从文件内部导入符号。被修复的部分是,你可以使用相对和“from”导入来自包内的模块(模块本身,不一定是其中的符号),在该包的__init__完成加载之前。 - Brendan Abel
3
如果在sys.modules中找不到'module_name',那么怎么样?import ... - jbryanscott
显示剩余12条评论

166

只导入模块,不要从模块中导入:

考虑 a.py

import b

class A:
    def bar(self):
        return b.B()

以及b.py:

import a

class B:
    def bar(self):
        return a.A()

这个完美地运作。


3
哇!谢谢,我知道把"from imports"中的一个放在模块底部可以解决循环导入错误,但这个方法更好! - Caumons
70
似乎不适用于子模块import foobar.mod_aimport foobar.mod_b与上述描述不一致。 - Nick
9
这也有一个很大的缺点:如果您删除了一个函数并忘记在某处更新对它的引用,那么您最终将面临运行时错误而不是导入时错误。 - ThiefMaster
26
社区:这个答案不够, 请阅读下面的 Brenden 的回答! - Dan Oblinger
1
@Nick,是的,除非您使用foobar.mod_a.function_foo(假设function_foo存在于mod_a中),否则不会。无论如何,我不建议这样做,只是指出一下。 - toto_tico
只有在没有顶层代码尝试访问导入模块的属性时,这才能避免问题。 - Karl Knechtel

9

我们采用绝对导入和函数的组合形式,以获得更好的阅读性和更短的访问字符串。

  • 优点:与纯粹的绝对导入相比,访问字符串更短
  • 缺点:由于额外的函数调用,有稍微更多的开销

主/子/a.py

import main.sub.b
b_mod = lambda: main.sub.b

class A():
    def __init__(self):
        print('in class "A":', b_mod().B.__name__)

main/sub/b.py

import main.sub.a
a_mod = lambda: main.sub.a

class B():
    def __init__(self):
        print('in class "B":', a_mod().A.__name__)

5
为什么要使用 lambda?为什么不直接使用 b_mod = main.sub.b - Brendan Abel
13
Lambda可以延迟访问,直到需要使用它。没有Lambda,会引发AttributeError错误。 - Izkata
4
请不要将Lambda表达式赋值给绑定名称,而是使用经典的def - TDk
1
请注意,将a_mod包装在函数中可能会产生一些意想不到的影响。例如,无论main.sub.a的类型是什么,type(a_mod)始终返回<class 'function'>。文档字符串也无法按预期工作。 - Zekko
为什么不用 as 别名呢?你可以这样导入:import main.sub.b as b_mod,然后使用更短的 b_mod。对于 a_mod 也是类似的。 - wow so much info

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接