Python 中循環 import 造成的問題如何解決?
- 業務模塊B
- 業務公用模塊BCommon(比如說推薦系統,可以推薦B,C...等等,所以要用到B中的Model)
在業務模塊B中,業務處理需要用到B.Model,而且推薦處理的時候需要import BCommon,就造成了:B -&> BCommon -&> B, 這種情況怎麼辦?
首先出現這種問題是因為沒有規劃好層級,哪些模塊和哪些模塊邏輯上應該在一起,哪些模塊是公共的依賴項,哪些是實際的業務代碼,按你的需求,顯然B.Model應該是一個獨立的模塊才對,把第一和第三層合成一個模塊然後把第二層獨立出去就會出現你這種情況。循環引用是小問題,代碼以後難維護、理不清實現的邏輯才是大問題。其次,Python根本就是可以循環引用的,只要你循環引用中的模塊並不是在定義階段就馬上使用,比如:
#module1.py
import module2
class Model(object):
def do_something(self):
module2.print_model(self)
#module2.py
import module1
def print_model(model):
print repr(module1.Model())
兩個import其中的一個在導入時會為空,取決於import順序,但由於只有在函數內部使用,只要import階段沒有執行到相應的位置就不會有問題。也就是說出現這種情況時只要避免三種使用方法:
- from ... import ... (這個去掉肯定沒啥問題)
- 直接執行的代碼(本來大部分情況下你都應該避免的)
- 類的繼承(基類的模塊去import派生類的模塊本來就很奇怪啊)
將import放到函數裡面,放到最下面,都可以解決問題,但治標不治本,治本的還是要重新劃分模塊,邏輯理順了就不會出現循環import
建立一個Model模塊,存放B.Model、C.Model等,大家都去import Model模塊
最好的做法是能不弄,就不要弄循環import,像go這種語言就由編譯器禁止你這麼搞,搞了就編譯錯誤py中循環import存在的問題主要在於對於沒有導入的模塊,import本身就是一次執行,就是把模塊代碼的global域當成正常代碼執行一次,這樣如果多個模塊的全局變數有循環依賴,就可能異常,比如:A.py:import Ba = B.aB.py:
import A
print A.aa = 123無論先import誰,都會報它沒有屬性a,這裡import,賦值,print都是py的語句,一條條執行比如從main.py裡面先import A:1 首次導入,A先進入sys模塊的module表2 由於是首次導入,從A開始執行,執行import B這句3 B首次導入,進入sys的module表4 從B.py的第一句開始,執行import A這句,由於A已經在表中,略過,然後執行print A.a,這時候A.py還沒執行到給a賦值的地方,所以異常避免這種情況一般就沒什麼事了,個人一個習慣和建議是,全局變數初值都賦值為不依賴其他模塊的值,比如None,整數等,如果需要依賴,模塊提供init函數,由main模塊在程序啟動時候手工按順序初始化好需要注意的是,和import是可執行語句一樣,def和class對於py都是可執行語句,def是把其下的代碼塊對應的函數對象關聯給函數名,等於一個賦值,class要更複雜一點,先把它下面的代碼塊當成一個普通函數執行,最後執行MAKE_CLASS位元組碼做convert再賦值給類名,其他的,修飾器也是可執行代碼,對py來說,一切都是指令,唯一一個「聲明」語法,是global
我最近寫了一篇博客,來說明這個問題,詳情見 如何避免Python的循環導入問題
總體來說,有四種方法:
1. 在module頂部引入,不要用from,相對引入,只在Python 2中有效
2. 在module的頂部引入,不要用from,絕對引入
3. 在module底部引入another module的attribute,而非another module,用from
4. 函數頂部引入,可以用from
-----------------------------
詳細說明如下:
1. 在module頂部引入,不要用from,相對引入,只在Python 2中有效
如:
# pkg/module_a.py
from __future__ import print_function
import module_b
def action_a():
print(module_b.action_b.__name__)
# pkg/module_b.py
from __future__ import print_function
import module_a
def action_b():
print(module_a.action_a.__name__)
2. 在module的頂部引入,不要用from,絕對引入
如:
# pkg/module_a.py
from __future__ import print_function
import pkg2.module_b
def action_a():
print(pkg2.module_b.action_b.__name__)
# pkg/module_b.py
from __future__ import print_function
import pkg2.module_a
def action_b():
print(pkg2.module_a.action_a.__name__)
3. 在module底部引入another module的attribute,而非another module,用from
如:
# pkg/module_a.py
from __future__ import print_function
def action_a():
print(action_b.__name__)
from .module_b import action_b
# pkg/module_b.py
from __future__ import print_function
def action_b():
print(action_a.__name__)
from .module_a import action_a
4. 函數頂部引入,可以用from
如:
# pkg/module_a.py
from __future__ import print_function
def action_a():
from . import module_b
print(module_b.action_b.__name__)
# pkg/module_b.py
from __future__ import print_function
def action_b():
from . import module_a
print(module_a.action_a.__name__)
更詳細的說明解釋可以看我的博客:如何避免Python的循環導入問題
相關代碼可以在GitHub上查看:flyhigher139/python_circular_import
- 上策,拆成不循環引用的
- 中策,把import放到最後
- 下冊,在函數裡面import
哈哈~這個時候大部頭就可以上場了。推薦看一下python核心編程的12.8.5導入循環。簡而言之,就是,因為import遵循作用域原則,將import放在需要它的作用域內。不想看書的可以看一下這篇python 導入循環問題
之前做項目的時候也遇到過這個問題:
最開始是每個模塊下面單獨的form、view、model,經常遇到兩個模塊相互引用model造成循環引用的問題。
後來仔細想想,劃分出來的「模塊」是一個真正獨立的模塊嗎?是的話為什麼還會import同級模塊呢?不是的話為什麼採用這種劃分方式呢?
所以後來就把每個所謂的「模塊」拆分開了,按照flask官方推薦的方式分成views、forms、models、errors、utils幾個模塊,模塊內還可以按照業務內容細分。
utils是框架之上最基礎的模塊,forms、models、errors等都是utils之上的基礎模塊,views是最終的頂級模塊,上級模塊可以調用下級模塊,下級模塊不能訪問上級模塊。
從此以後再也沒遇到過循環引用的問題。為啥我寫的代碼中就沒有這個問題?是我寫的項目太簡單?
一句話在函數內使用到的地方再import
@小太陽 python核心編程的12.8.5導入循環實際上,在使用 Python 時, 你會發現是能夠導入循環的。 如果你開發了大型的 Python 工程,那麼你很可能會陷入這樣的境地。
我們來看一個例子。 假定我們的產品有一個很複雜的命令行介面( command-line interface ,
CLI)。 其中將會有超過一百萬的命令, 結果你就有了一個「超冗餘處理器」(overly massive handler,
Edit By Vheavens
Edit By Vheavens
OMH)子集。 每加入一個新特性, 將有一到三條的新命令加入, 用於支持新的特性。 下邊是我們的
omh4cli.py 腳本:
from cli4vof import cli4vof
# command line interface utility function
def cli_util():
pass
# overly massive handlers for the command line interface
def omh4cli():
:
cli4vof()
:
omh4cli()
假定大多控制器都要用到這裡的(其實是空的)工具函數。命令行介面的 OMH 都被封裝在
omh4cli() 函數里。 如果我們要添加一個新的命令, 那麼它會被調用。
現在這個模塊不斷地增長, 一些聰明的工程師會決定把新命令放入到隔離的模塊里, 在原始模
塊中只提供訪問新東西的鉤子。 這樣, 管理代碼會變得更簡單, 如果在新加入內容中發現了 bug ,
那麼你就不必在一個幾兆的 Python 文件里搜索。
在我們的例子中, 有一個興奮的經理要我們加入一個 "非常好的特性"。我們將創建一個新的
cli4vof.py 腳本, 而不是把新內容集成到 omh4cli.py 里:
import omh4cli
# command-line interface for a very outstanding feature
def cli4vof():
omh4cli.cli_util()
前邊已經提到, 工具函數是每個命令必須的, 而且由於不能把代碼從主控制器複製出來, 所以
我們導入了主模塊, 在我們的控制器中添加對 omh , omh4cli() 的調用。
問題在於主控制器 omh4cli 會導入我們的 cli4vof 模塊(獲得新命令的函數), 而 cli4vof
也會導入 omh4cli (用於獲得工具函數)。模塊導入會失敗, 這是因為 Python 嘗試導入一個先前沒
有完全導入的模塊:
$ python omh4cli.py
Traceback (most recent call last):
File "omh4cli.py", line 3, in ? from cli4vof import cli4vof
File "/usr/prod/cli4vof.py", line 3, in ?
import omh4cli
File "/usr/prod/omh4cli.py", line 3, in ?
from cli4vof import cli4vof
Edit By Vheavens
Edit By Vheavens
ImportError: cannot import name cli4vof
注意跟蹤返回消息中顯示的對 cli4vof 的循環導入。 問題在於要想調用工具函數, cli4vof 必
須導入 omh4cli 。 如果它不需要這樣做, 那麼 omh4cli 將會成功導入 cli4vof , 程序正常執行。
但在這裡, omh4cli 嘗試導入 cli4vof , 而 cli4vof 也試著導入 omh4cli 。 最後誰也不會完成
導入工作, 引發錯誤。 這只是一個導入循環的例子。 事實上實際應用中會出現更複雜的情況。
解決這個問題幾乎總是移除其中一個導入語句。 你經常會在模塊的最後看到 import 語句。作
為一個初學者, 你只需要試著習慣它們, 如果你以前遇到在模塊底部的 import 語句,現在你知道是
為什麼了.。在我們的例子中, 我們不能把 import omh4cli 移到最後, 因為調用 cli4vof() 的時
候 omh4cli() 名字還沒有被載入。
$ python omh4cli.py
Traceback (most recent call last): File "omh4cli.py", line 3, in ? from cli4vof import
cli4vof
File "/usr/prod/cli4vof.py", line 7, in ?
import omh4cli
File "/usr/prod/omh4cli.py", line 13, in ?
omh4cli()
File "/usr/prod/omh4cli.py", line 11, in omh4cli cli4vof()
File "/usr/prod/cli4vof.py", line 5, in cli4vof omh4cli.cli_util()
NameError: global name "omh4cli" is not defined
我們的解決方法只是把 import 語句移到 cli4vof() 函數內部:
def cli4vof():
import omh4cli
omh4cli.cli_util()
這樣, 從 omh4cli() 導入 cli4vof() 模塊會順利完成, 在 omh4cli() 被調用之前它會被正
確導入。 只有在執行到 cli4vof.cli4vof() 時候才會導入 omh4cli 模塊。
推薦閱讀:
※你見過的最出色的程序員是怎樣的?為什麼出色?
※人文社科類專業的學生有學習編程的必要嗎?
※用 Unity 3D 開發遊戲,熟悉 C 語言和 C++ 是否重要?
※自學編程怎麼開始?
※偽程序員想學腳本語言,該如何選?