循环模块依赖和在Python中的相对导入

假设我们有两个具有循环依赖性的模块:
# a.py
import b
def f(): return b.y
x = 42
# b.py
import a
def g(): return a.x
y = 43

这两个模块在目录pkg中有一个空的__init__.py。导入pkg.a或pkg.b工作正常,如this answer中所述。如果我将导入更改为相对导入

from . import b

当尝试导入其中一个模块时,我收到一个ImportError:

>>> import pkg.a
Traceback (most recent call last):
  File "<stdin>",line 1,in <module>
  File "pkg/a.py",in <module>
    from . import b
  File "pkg/b.py",in <module>
    from . import a
ImportError: cannot import name a

为什么会收到此错误?是不是与上面的情况大致相同? (这是与this question相关吗?)

编辑:这个问题不是关于软件设计。我知道避免循环依赖的方法,但我对错误的原因感兴趣。

首先让我们从python中的导入工作开始:

首先让我们来看一下字节码:

>>> 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

hmm有趣:),所以从foo导入栏翻译成第一个IMPORT_NAME foo,它等效于import foo,然后IMPORT_FROM bar。

现在什么IMPORT_FROM做?

让我们看看什么python做的时候,他发现IMPORT_FROM

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()与这两个参数:

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获取属性名称,如果它不存在它提高ImportError否则返回此属性

在这与相对导入有什么关系?

好的相对导入喜欢从。 import b例如在OP问题中来自pkg import b的情况下是等价的。

但是这是怎么回事?要理解这一点,我们应该看看import.c模块的python专门的功能get_parent().正如你看到的功能是安静的长列表这里,但一般来说,它看到一个相对导入是做的是尝试更换点。与父包取决于__main__模块,这是再次从OP问题是包pkg。

现在让我们把所有这些在一起,并试图找出为什么我们结束在OP问题的行为。

这将帮助我们,如果我们可以看到什么python做进口时,这是我们的幸运日python已经有这个功能,可以通过运行它在额外的详细模式-vv启用。

所以使用命令行:python -vv -c’import pkg.b’:

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>",in <module>
  File "pkg/b.py",in <module>
    from . import a
  File "pkg/a.py",line 2,in <module>
    from . import a
ImportError: cannot import name a
# clear __builtin__._

什么刚刚发生之前ImportError?

第一)从。 import a in pkg / b.py被调用,它被转换为如上所述从pkg import a,这又是在bytecode等价于import pkg; getattr(pkg,’a’)。但等一下,一个模块呢?
在这里来了有趣的部分,如果我们有一些像从模块|包导入模块在这种情况下,第二次导入将发生,这是在import子句中的模块的导入。所以再次在OP示例中,我们现在需要导入pkg / a.py,首先我们在sys.modules中设置一个新模块的关键字pkg.a,然后继续我们的解释模块pkg / a.py,但在模块pkg / a.py完成导入它之前调用。进口b。

现在来第二)部分,pkg / b.py将被导入,它将首先尝试导入pkg,因为pkg已经导入,所以在我们的sys.modules中有一个键pkg,它将只返回值那个键。然后它将导入b在sys.modules中设置pkg.b键并开始解释。我们到达这条线路。导入!

但是请记住,pkg / a.py已经导入了,这意味着(sys.modules中的’pkg.a’)== True,所以导入将被跳过,只有getattr(pkg,’a’)被调用,会发生什么? python没有完成导入pkg / a.py!所以只有getattr(pkg,’a’)被调用,这将在import_from()函数中引发一个AttributeError,它将被转换为ImportError(不能导入名称a)。

免责声明:这是我自己的努力,以了解解释器内发生的事情,我很远是一个专家。

EDIt:这个答案是改写,因为当我试图再读一遍我说我的答案是坏配方,希望现在它会更有用:)

相关文章

迭代器模式(Iterator)迭代器模式(Iterator)[Cursor]意图...
高性能IO模型浅析服务器端编程经常需要构造高性能的IO模型,...
策略模式(Strategy)策略模式(Strategy)[Policy]意图:定...
访问者模式(Visitor)访问者模式(Visitor)意图:表示一个...
命令模式(Command)命令模式(Command)[Action/Transactio...
生成器模式(Builder)生成器模式(Builder)意图:将一个对...