<p>要理解循环依赖关系,需要记住Python本质上是一种脚本语言。在编译时执行方法外部的语句。Import语句的执行与方法调用一样,要理解它们,您应该像方法调用一样考虑它们。</p>
<p>当您执行导入时,会发生什么情况取决于您要导入的文件是否已存在于模块表中。如果是,Python将使用符号表中当前的任何内容。否则,Python将开始读取模块文件,编译/执行/导入在其中找到的任何内容。编译时引用的符号是否被找到,取决于它们是否被编译器看到或尚未被编译器看到。</p>
<p>假设您有两个源文件:</p>
<p>文件X.py</p>
<pre><code>def X1:
return "x1"
from Y import Y2
def X2:
return "x2"
</code></pre>
<p>文件Y.py</p>
<pre><code>def Y1:
return "y1"
from X import X1
def Y2:
return "y2"
</code></pre>
<p>现在假设您编译文件X.py。编译器首先定义方法X1,然后在X.py中命中import语句。这将导致编译器暂停编译X.py并开始编译Y.py。此后不久,编译器将在Y.py中命中import语句。由于X.py已经在模块表中,所以Python使用现有的不完整的X.py符号表来满足所请求的任何引用。X.py中import语句之前出现的任何符号现在都在符号表中,但之后出现的任何符号都不在符号表中。因为X1现在出现在import语句之前,所以它被成功导入。然后Python继续编译Y.py。这样就定义了Y2并完成了Y.py的编译。然后继续编译X.py,并在Y.py符号表中找到Y2。编译最终完成,没有错误。</p>
<p>如果试图从命令行编译Y.py,就会发生非常不同的情况。编译Y.py时,编译器在定义Y2之前命中import语句。然后开始编译X.py。很快它就会在X.py中命中需要Y2的import语句。但是Y2是未定义的,所以编译失败。</p>
<p>请注意,如果将X.py修改为导入Y1,则无论编译哪个文件,编译都将始终成功。但是,如果修改文件Y.py以导入符号X2,则两个文件都不会编译。</p>
<p>当模块X或X导入的任何模块可能导入当前模块时,请不要使用:</p>
<pre><code>from X import Y
</code></pre>
<p>当您认为可能存在循环导入时,还应避免编译时引用其他模块中的变量。考虑一下看起来无辜的代码:</p>
<pre><code>import X
z = X.Y
</code></pre>
<p>假设模块X在该模块导入X之前导入该模块。进一步假设Y是在import语句之后的X中定义的。然后,在导入此模块时将不定义Y,您将得到编译错误。如果这个模块先导入Y,你就可以不用管它了。但是当你的一个同事无辜地改变了第三个模块中定义的顺序时,代码就会崩溃。</p>
<p>在某些情况下,可以通过将import语句向下移动到其他模块所需的符号定义之下来解决循环依赖关系。在上面的例子中,import语句之前的定义永远不会失败。import语句之后的定义有时会失败,这取决于编译的顺序。您甚至可以将import语句放在文件的末尾,只要编译时不需要任何导入的符号。</p>
<p>请注意,在模块中向下移动import语句会模糊您正在执行的操作。请在模块顶部添加如下注释来对此进行补偿:</p>
<pre><code>#import X (actual import moved down to avoid circular dependency)
</code></pre>
<p>一般来说,这是一种不好的做法,但有时很难避免。</p>