1. 程式人生 > >Python的循環導入問題

Python的循環導入問題

oba 編譯過程 目錄結構 dem globals name .org 註意 應該

循環導入的最好的解決方法是從架構上優化,即調整模塊和模塊成員變量的設計。一個好的原則是:可導出的成員變量,都不應該依賴於導入進來的成員變量。
但是在業務開發的過程中,總會遇到通過架構層面解決不了的導入問題,這時候就只能通過語言層面來解決了。

目錄結構(下面的案例的目錄結構都是這樣的):

root.py
/pack1
    __init__.py
    module_a.py
/pack2
    __init__.py
    module_b.py
module_c.py
module_d.py

循環導入例子

首先看一下什麽是循環導入和循環導入的原因。
root.py

from pack1.module_a import class_a

module_a.py

print "start init module a"
from pack2.module_b import class_b
class class_a():
    def f(self):
        class_b
print "init module a"

module_b.py

print "start init module b"
from pack1.module_a import class_a
class class_b():
    def f(self):
        class_a
print "init module b"

會報錯:

start init module a
start init module b
Traceback (most recent call last):
  File "E:/my_demo/demo2016/b???????/s2/root.py", line 2, in <module>
    from pack1.module_a import class_a
  File "E:\my_demo\demo2016\b???????\s2\pack1\module_a.py", line 2, in <module>
    from pack2.module_b import class_b
  File "E:\my_demo\demo2016\b???????\s2\pack2\module_b.py", line 2, in <module>
    from pack1.module_a import class_a
ImportError: cannot import name class_a

代碼執行的流程:

  1. 執行root.py的from pack1.module_a import class_a,發現需要導入模塊module_a
  2. 一個空的字典會被創建,對應module_a的globals
  3. module_a的代碼會被執行,當執行到from pack2.module_b import class_b時,發現需要導入模塊module_b
  4. 一個空的字典會被創建,對應module_b的globals
  5. module_b的代碼會被執行,當執行到from pack1.module_a import class_a時,發現需要導入模塊module_a,但是此時已經有module_a的globals了,所以直接訪問字典裏的class_a,但是由於module_a的globals還是空的,即裏面沒有class_a,所以拋出異常

參考文檔
所以根本原因是:在導入的時候,module_b需要訪問module_a的變量class_a,但是class_a沒有初始化完成
所以解決方法有兩個:

  1. 在導入的時候,讓module_b不要訪問module_a的變量,也就是方案一
  2. class_a初始化完成後,才讓module_b訪問module_a的變量,也就是方案二和三

方案一、使用import ...代替 from...import...

root.py

import pack1.module_a

module_a.py

print "start init module a"
import pack2.module_b 
class class_a():
    def f(self):
        m_b.class_b
print "init module a"
if __name__ == '__main__':
    pass

module_b.py

print "start init module b"
import pack1.module_a 
class class_b():
    def f(self):
        pack1.module_a.class_a
print "init module b"

module_a和module_b都會被編譯,終端會輸出:

start init module a
start init module b
init module b
init module a

即首先編譯a,編譯過程中發現需要編譯b,編譯b完成後,編譯a剩下的部分、

這個案例不使用from....import....,而使用import,這樣是可以成功循環導入的,不過一個缺點是,每次訪問module的時候,都需要寫全路徑,例如pack1.module_a.class_a,非常繁瑣。
一個優化的方案是導入的時候,使用import....as... 例如:import pack1.module_a as m_a。但是很奇怪的是,在module_a中可以這樣用,但是在module_b中不可以,否則就會導致報錯。還有如果把roo.py改為import pack2.module_b,就會反過來,即module_b中可以這樣用,但是在module_a中不可以。所以準確點應該是在root.py導入的模塊中可以使用,但是在其他模塊不能使用。所以import....as...這個方案並不好。
註意,import...只能import到模塊,不能import模塊裏面的成員變量,例如import pack1.module_a.class_a 是不可以的

這個方案的缺點就是訪問模塊裏面的成員變量太繁瑣

方案二、把導入放在後面

root.py

from pack1.module_a import class_a

module_a.py

print "start init module a"
#from pack2.module_b import class_b #放在這裏會報錯
class class_a():
    def f(self):
        # m_b.class_b
        pass

from pack2.module_b import class_b #放在這裏不會
class class_c():
    def f(self):
        class_b
print "init module a"

module_b.py

print "start init module b"
from pack1.module_a import class_a
class class_b():
    def f(self):
        class_a
print "init module b"

當存在類似的依賴關系:class_c依賴class_b依賴class_a,然後class_a和class_c在同一個模塊時,可以使用這種方案。
from pack2.module_b import class_b這句放在class_a後面,這樣在module_b中訪問module_a.class_a是成功的,因為class_a的定義代碼已經執行完成,並被添加到module_a的globals中。

方案三、把導入語句放在語句塊中

root.py

from pack1.module_a import func_a

print 'root start run func a'
func_a()
print 'root end run func a'

module_a.py

print "start init module a"

def func_a():
    from pack2.module_b import func_b
    func_b()
    print 'run func a'
print "init module a"

module_b.py

print "start init module b"

def func_b():
    from pack1.module_a import func_a
    print 'run func b'

print "init module b"

輸出:

start init module a
init module a
root start run func a
start init module b
init module b
run func b
run func a
root end run func a

在需要使用func_b的時候,才進行導入操作,這樣在執行module_b的時候,module_a已經初始化完成,module_a的globals已經有func_a了,所以導入不會報錯。

查看已經導入的module情況

import sys
from pack1.module_a import func_a
print sys.modules  
# {'pack1': <module 'pack1' from 'E:\my_demo\demo2016\b???????\s4\pack1\__init__.pyc'>,}
print sys.modules['pack1.module_a'].__dict__
# {'func_a': <function func_a at 0x0254FB30>, '__doc__': None}
sys.modules['pack1.module_a'].func_a_tmp=sys.modules['pack1.module_a'].func_a

通過sys.modules可以訪問所有當前已導入的模塊。
modules是一個字典,key是模塊的路徑,例如pack1.module_a,value是一個模塊對象
模塊對象中,屬性名是模塊中全局變量的名字,即sys.modules[‘pack1.module_a‘].__dict__等於module_a裏面的globals()

所以,當在module_b中執行from pack1.module_a import class_a時,相當於執行代碼:

import sys
if 'pack1.module_a' in sys.modules:
    if hasattr(sys.modules['pack1.module_a'],"class_a"):
        sys.modules['pack2.module_b'].class_a=sys.modules['pack1.module_a'].class_a
    else:
        raise Exception(u"循環導入異常")
else:
    #執行導入pack1.module_a的操作,也就是初始化一個module對象,然後令sys.modules['pack1.module_a']=這個對象

所以解決循環導入的問題,就相當於使上面的代碼不會執行到raise Exception(u"循環導入異常")這一句,方案一和方案二都是通過這種方法解決的。

未經允許,請不要轉載

Python的循環導入問題