假设我们有两个具有循环依赖性的模块:
1 2 3 4
| # a.py
import b
def f(): return b.y
x = 42 |
1 2 3 4
| # b.py
import a
def g(): return a.x
y = 43 |
号
这两个模块位于目录pkg中,其中有一个空的__init__.py。如本答案所述,进口pkg.a或pkg.b是可行的。如果我把进口改为相对进口
我在尝试导入其中一个模块时得到了一个ImportError:
1 2 3 4 5 6 7 8
| >>> import pkg.a
Traceback (most recent call last):
File"<stdin>", line 1, in <module>
File"pkg/a.py", line 1, in <module>
from . import b
File"pkg/b.py", line 1, in <module>
from . import a
ImportError: cannot import name a |
。
为什么会出现此错误?情况和上面的差不多吗?(这与这个问题有关吗?)
编辑:这个问题不是关于软件设计的。我知道如何避免循环依赖,但我还是对错误的原因感兴趣。
- 您能详细说明在更改为from . import b时收到的ImportError的详细信息吗?
- 你如何练习这个代码?你在pkg中有没有其他的模块,类似于:if __name__ == 'main': from . import a?如果是这样,您可能希望阅读PEP 366,这个PEP使使用来自可执行模块和包的相对导入变得更加容易。
- 如果a依赖于b,b依赖于a,为什么不将两者合并到同一个文件中呢?
- @帕维尔:我添加了错误消息的完整回溯。在实际库中,__init__.py从a和b导入名称,使用包的程序导入包。
- @jbernardo:这个文件会变得非常大,实际上功能已经很好地发挥了。b在一些数据管道中实现了稍后的处理步骤,并对a中的类型进行操作。在a中,我向将调用转发到b中的函数的类中添加了一些方便的方法。除此之外,我想对上述行为作出解释。
- 或者把这些方便的方法放到第三个模块中,这个模块同时依赖于A和B。
- @jbernardo:设计其实很干净,一点也不坏。有一些解决方案会打破这个循环,但它们都不如当前的解决方案好。但如前所述——我问这个问题是因为我对答案感兴趣。
- @约翰:它们是在a中定义的类上的方法。我不想以后再把它们补上。派生类不会这样做,因为a中的函数从a返回类,而这些类应该有方法。如果我从c返回类,我将再次具有循环依赖性。不管怎样,所有这些都有失重点。我对这个错误的解释很感兴趣。
- @Sven我不能回答为什么您的代码不工作,但是正如我们所说,循环依赖性是已知的软件反模式,应该避免。
- @jbernardo:嗯,我知道这一点,在介绍这个循环之前,我仔细考虑了一下。我决定,在本例中,通过便利方法改进接口胜过打破一些抽象原则。请注意,如果我加入文件,循环依赖项不会消失——它只是单个文件中的循环依赖项。
- @Sven:我在2.6 python上使用了各种方法来编写import语句。我用python -c"import pkg.a"行使进口权。我可以报告,from pkg import ...和from . import ...表格不起作用,但import a和import b起作用。似乎只有在后一种情况下才允许进口周期。为什么,我不知道。投票表决这个问题。
- python的可能副本:循环(或循环)导入
首先,让我们从from import在Python中的工作方式开始:
首先让我们看看字节码:
1 2 3 4 5 6 7 8 9 10 11 12
| >>> def foo():
... from foo import bar
>>> dis.dis(foo)
2 0 LOAD_CONST 1 (-1)
3 LOAD_CONST 2 (('bar',))
6 IMPORT_NAME 0 (foo)
9 IMPORT_FROM 1 (bar)
12 STORE_FAST 0 (bar)
15 POP_TOP
16 LOAD_CONST 0 (None)
19 RETURN_VALUE |
嗯,有趣的是:),所以from foo import bar被翻译成第一个IMPORT_NAME foo,相当于import foo,然后是IMPORT_FROM bar。
现在,IMPORT_FROM做了什么?
让我们看看当Python发现IMPORT_FROM时,他做了什么:
1 2 3 4 5 6 7 8 9
| TARGET(IMPORT_FROM)
w = GETITEM(names, oparg);
v = TOP();
READ_TIMESTAMP(intr0);
x = import_from(v, w);
READ_TIMESTAMP(intr1);
PUSH(x);
if (x != NULL) DISPATCH();
break; |
号
基本上,他得到了要从中导入的名称,在我们的foo()函数中将是bar,然后他从帧堆栈中弹出值v,这是最后一个执行的操作码(IMPORT_NAME的返回值,然后用这两个参数调用函数import_from():
1 2 3 4 5 6 7 8 9 10 11 12
| static PyObject *
import_from(PyObject *v, PyObject *name)
{
PyObject *x;
x = PyObject_GetAttr(v, name);
if (x == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) {
PyErr_Format(PyExc_ImportError,"cannot import name %S", name);
}
return x;
} |
如您所见,import_from()函数非常简单,它首先尝试从模块v中获取属性name,如果不存在,则引发ImportError,否则返回该属性。
现在这与相对进口有什么关系?
比如说,像from . import b这样的相对进口与from pkg import b那样的相对进口是等价的。
但这是怎么发生的?为了理解这一点,我们应该特别关注python的import.c模块,以及函数get_parent()。正如您所看到的,函数在这里列出的时间很长,但一般来说,当它看到相对导入时,它所做的是尝试用依赖于__main__模块的父包替换dot .,这也是从op问题中得到的包pkg。
现在,让我们把所有这些放在一起,试着弄明白为什么我们最终会出现在操作问题中的行为。
为此,如果我们能够看到python在执行导入操作时所做的操作,那么这将有助于我们的幸运日:python已经具备了这个功能,可以通过在额外的详细模式-vv下运行它来启用它。
所以使用命令行:python -vv -c 'import pkg.b':
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| Python 2.6.5 (r265:79063, Apr 16 2010, 13:57:41)
[GCC 4.4.3] on linux2
Type"help","copyright","credits" or"license" for more information.
import pkg # directory pkg
# trying pkg/__init__.so
# trying pkg/__init__module.so
# trying pkg/__init__.py
# pkg/__init__.pyc matches pkg/__init__.py
import pkg # precompiled from pkg/__init__.pyc
# trying pkg/b.so
# trying pkg/bmodule.so
# trying pkg/b.py
# pkg/b.pyc matches pkg/b.py
import pkg.b # precompiled from pkg/b.pyc
# trying pkg/a.so
# trying pkg/amodule.so
# trying pkg/a.py
# pkg/a.pyc matches pkg/a.py
import pkg.a # precompiled from pkg/a.pyc
# clear[2] __name__
# clear[2] __file__
# clear[2] __package__
# clear[2] __name__
# clear[2] __file__
# clear[2] __package__
...
Traceback (most recent call last):
File"<string>", line 1, in <module>
File"pkg/b.py", line 1, in <module>
from . import a
File"pkg/a.py", line 2, in <module>
from . import a
ImportError: cannot import name a
# clear __builtin__._ |
。
嗯,在ImportError之前发生了什么?
首先)pkg/b.py中的from . import a被称为EDOCX1,如上文所述,它被翻译成from pkg import a,它在字节码中又等同于import pkg; getattr(pkg, 'a')。但是等一下,a也是一个模块吗?!好吧,有趣的是,如果我们有类似from module|package import module的东西,在这种情况下,会发生第二次导入,即导入条款中的模块导入。因此,在操作示例中,我们现在需要导入pkg/a.py,正如您所知,首先我们在sys.modules中设置了新模块的密钥,该模块将是pkg.a,然后我们继续解释该模块pkg/a.py,但在该模块pkg/a.py完成导入之前,它称为from . import b。
接下来是第二部分,pkg/b.py将被导入,然后它将首先尝试import pkg,因为pkg已被导入,所以我们的sys.modules中有一个密钥pkg,它将返回该密钥的值。然后将import b设置pkg.b键在sys.modules中,开始解释。我们到了这条线from . import a!
但请记住,pkg/a.py已经进口了,这意味着('pkg.a' in sys.modules) == True将被跳过进口,只调用getattr(pkg, 'a'),但会发生什么?python没有完成EDOCX1的导入(31)!因此,只调用getattr(pkg, 'a'),这将在import_from()函数中引发AttributeError,该函数将转换为ImportError(cannot import name a)。
免责声明:这是我自己的努力去理解口译员内部发生的事情,我远不是一个专家。
编辑:这个答案被改写了,因为当我再次尝试阅读它时,我注意到我的答案是如何表达的,希望现在它会更有用:)
- 感谢您提供详细的答案,并实际查看了这些信息来源。我还是不太满意。有两件事让我困惑:1.在你的观点(1)中,执行from pkg import b。为什么即使b显然不在pkg的名称空间中,它也能工作?这不是和下一步的情况一样吗,from pkg import a?2。模块的循环依赖性通常起作用,因为在执行模块体之前,空模块对象被插入到sys.modules中。为什么在执行模块体之前,空模块对象a没有插入到pkg中?
- @七点一分。因为pkg.b不是sys.modules所以我们需要导入它,首先在sys.modules中设置pkg.b条目,然后在导入成功后将b添加到pkg的名称空间。2。a模块插入包命名空间中导入的末尾,因为如果a的导入失败(您可以在模块中引发异常并查看),我们不希望它在pkg的命名空间中挂起。基本上,将导入的模块添加到包的名称空间是在导入成功之后完成的。
- 感谢你为这个答案付出的一切努力。现在,它让事情变得非常清楚。
- @这是我的荣幸,也是一个非常好的挑战:)
(顺便说一下,相对进口并不重要。使用from pkg import…显示相同的异常。)
我想这里的情况是,from foo import bar和import foo.bar的区别在于,在第一个函数中,bar的值可以是pkg foo中的module,也可以是foo中的变量。在第二种情况下,bar除了模块/包之外,其他任何东西都是无效的。
这很重要,因为如果已知bar是一个模块,那么sys.modules的内容就足以填充它。如果它可能是foo模块中的一个变量,那么解释器实际上必须查看foo的内容,但是在导入foo时,这是无效的;实际的模块还没有被填充。
在相对导入的情况下,我们理解from . import bar是指从包含当前模块的包中导入BAR模块,但这实际上只是语法上的糖分,.的名称被转换为完全限定的名称并传递给__import__(),因此它看起来像ambigious from foo import bar这样的世界。
- 谢谢你的回答。不幸的是,我认为这不能解释这一点。如果在导入时看foo的内容是无效的,那么非循环导入也会失败:如果foo.bar看from foo import quux的内容,而我导入foo.bar的内容,那么解释器在导入时也要看foo的内容,这样做很好。我说得有道理吗?
作为补充说明:
我有以下模块结构:
1 2 3 4 5 6
| base
+guiStuff
-gui
+databaseStuff
-db
-basescript |
我想用import base.basescript运行我的脚本,但是由于gui文件有一个import base.databaseStuff.db文件,导致了base的导入,所以失败了,出现了一个错误。由于base只注册为__main__号,导致第二次执行整个导入操作以及上述错误,除非我使用了base号上方的外部脚本,因此只导入base号/basescript号。为了防止出现这种情况,我在基本脚本中添加了以下内容:
1 2 3 4
| if __name__ == '__main__' or \
not '__main__' in sys.modules or \
sys.modules['__main__'].__file__ != __file__:
#imports here |
。