大型項(xiàng)目中為了維護(hù)方便,通常使用模塊化開(kāi)發(fā),模塊化的過(guò)程中,就會(huì)涉及到各種包或者模塊的相互導(dǎo)入,即使是對(duì)于有多個(gè)項(xiàng)目的Python開(kāi)發(fā)者來(lái)說(shuō), import 也會(huì)讓人困惑!本文帶你深入了解python中 import 的內(nèi)在機(jī)制,從而避免import導(dǎo)入引發(fā)的異常。
任何 .py 文件都可以稱為模塊
可以將多個(gè)模塊放入一個(gè)包中,就像電腦中的文件夾,但與文件夾的區(qū)別是,package包含 __init__.py 文件
當(dāng)我們執(zhí)行 python xx.py 時(shí),python是如何幫我們正確定位包所在的目錄呢?其實(shí)系統(tǒng)是按照以下順序來(lái)尋找的:
1.系統(tǒng)內(nèi)置模塊,比如os, sys模塊2.入口文件所在的目錄,比如main.py所在的目錄3.Python環(huán)境變量,也就是我們平時(shí)pip install后的包所在的目錄,如Anaconda下的site-packages目錄
在Python中,如果遇到了import錯(cuò)誤,我們可以通過(guò)以下命令查看搜索路徑:
import sysprint(sys.path)
結(jié)果:
sys.path: ['/Users/root/Python/project','/Users/root/anaconda3/lib/python36.zip','/Users/root/anaconda3/lib/python3.6','/Users/root/anaconda3/lib/python3.6/lib-dynload','/Users/root/.local/lib/python3.6/site-packages','/Users/root/anaconda3/lib/python3.6/site-packages','/Users/root/anaconda3/lib/python3.6/site-packages/Sphinx-1.5.6-py3.6.egg','/Users/root/anaconda3/lib/python3.6/site-packages/aeosa','/Users/root/anaconda3/lib/python3.6/site-packages/mdr-0.0.1-py3.6-macosx-10.7-x86_64.egg']
可以看到,其中第一個(gè)目錄是我們運(yùn)行的文件所在目錄,其它都是Python環(huán)境變量中的目錄
直接被Python解釋器運(yùn)行的文件,稱之為程序的 入口文件 ,在Python中,入口文件有且僅有一個(gè)
我們經(jīng)常使用以下語(yǔ)句:
# in main.pyif __name__=='__main__': run()
__name__=='__main__' 這個(gè)語(yǔ)句就是檢測(cè)當(dāng)前腳本是否被當(dāng)作入口文件使用,如果被當(dāng)做入口文件使用,那么就運(yùn)行 if __name__=='__main__': 下面的代碼塊,如果只是當(dāng)做模塊被其他的模塊 import 進(jìn)去使用,那就不應(yīng)該運(yùn)行這部分代碼塊。
因此,判斷一個(gè)文件是否被python解釋器當(dāng)做入口文件對(duì)待,是可以打印腳本的 __name__ 變量的。
# in main.pyprint(__main__)
如果我們把 xx.py 當(dāng)做入口文件,那么Python解釋器,就會(huì)將 xx.py 所在的目錄,加入到sys.path中,作為import時(shí)搜索的根目錄。簡(jiǎn)言之,你用 python filename.py 執(zhí)行哪個(gè)文件,Python解釋器就會(huì)將哪個(gè)文件所在目錄加入 import 的搜索目錄。
這引入了一個(gè)項(xiàng)目結(jié)構(gòu)合理性的問(wèn)題: 入口文件的目錄層級(jí)應(yīng)該是頂層的 ,也就是入口文件目錄層級(jí)不應(yīng)該低于任何模塊或者包,一個(gè)常見(jiàn)的目錄結(jié)構(gòu)通常如下:
$ tree./project├── package│ ├── __init__.py│ ├── sub_pkg1│ │ ├── __init__.py│ │ ├── module_X.py│ │ └── module_Y.py│ └── sub_pkg2│ ├── __init__.py│ └── module_Z.py└── main.py(入口文件)
如果我們執(zhí)行 python main.py ,那么main.py就會(huì)被當(dāng)做入口文件,所在目錄 project 就會(huì)被加入import的搜索路徑,那么我們將能夠搜索 package1 和 package2 中的模塊;現(xiàn)在假設(shè)我們直接運(yùn)行 python module_X.py ,此時(shí)入口文件變成了 module_X.py ,那么我們 import 搜索的根目錄為 sub_pkg1 ,當(dāng)我們?cè)噲D去 import sub_pkg2 中的文件時(shí),就會(huì)引發(fā)異常。
答案是:通過(guò) python -m sub_pkg1.module_X 來(lái)運(yùn)行!
python -m 的意思是將module當(dāng)做模塊來(lái)運(yùn)行,同時(shí) sub_pkg1.module_X 是導(dǎo)入路徑,這樣子就不會(huì)找不到模塊了。
如果在一個(gè)大型項(xiàng)目中,你經(jīng)常使用 cd 命令跳轉(zhuǎn)到各種不同層級(jí)的目錄中 Python xx.py 運(yùn)行某個(gè)模塊,那你可能得嘗試使用 Python -m 了,也就是我們最好在項(xiàng)目的根目錄下完成所有的模塊的測(cè)試。當(dāng)然,完善的項(xiàng)目,應(yīng)該有專門的測(cè)試目錄如 tests , 這個(gè)以后有機(jī)會(huì)再講。
在Python中,存在相對(duì)導(dǎo)入和絕對(duì)導(dǎo)入兩種import機(jī)制,但無(wú)論是絕對(duì)導(dǎo)入還是相對(duì)導(dǎo)入,都需要一個(gè)參照物,不然「絕對(duì)」與「相對(duì)」的概念就無(wú)從談起。絕對(duì)導(dǎo)入的參照物是項(xiàng)目的根文件夾,相對(duì)導(dǎo)入?yún)⒄瘴锸钱?dāng)前模塊,當(dāng)我們使用相對(duì)導(dǎo)入時(shí),需要給出相對(duì)于當(dāng)前模塊,想導(dǎo)入模塊所在的位置。
# 相對(duì)導(dǎo)入from . import foolfrom .package import foolfrom ..module import spam# 絕對(duì)導(dǎo)入,項(xiàng)目根目錄是appfrom app.package import fool
相對(duì)導(dǎo)入可以避免硬編碼帶來(lái)的維護(hù)問(wèn)題,例如我們改了某一頂層包的名字,那么其子包所有的導(dǎo)入就都不能用了。除非我們手動(dòng)修改頂層包名。 但是存在相對(duì)導(dǎo)入語(yǔ)句的模塊,不能直接使用 python xx.py 的方式運(yùn)行,否則會(huì)有異常:
ValueError: Attempted relative import in non-package
·如果是絕對(duì)導(dǎo)入,一個(gè)模塊只能導(dǎo)入自身的子模塊或和它的頂層模塊同級(jí)別的模塊及其子模塊·如果是相對(duì)導(dǎo)入,一個(gè)模塊必須有 包結(jié)構(gòu) (意味著有 __init__.py )且只能導(dǎo)入它的頂層模塊內(nèi)部的模塊 所以,如果一個(gè)模塊被直接運(yùn)行,Python會(huì)將該模塊當(dāng)做 頂層模塊(top level) ,不再當(dāng)做一個(gè)包來(lái)對(duì)待,因此不存在層次結(jié)構(gòu),所以找不到其他的相對(duì)路徑,所以如果直接運(yùn)行python xx.py ,而xx.py有相對(duì)導(dǎo)入就會(huì)報(bào)錯(cuò)
看下面例子:
$ tree./project├── package│ ├── __init__.py│ ├── sub_pkg1│ │ ├── __init__.py│ │ ├── module_X.py│ │ └── module_Y.py│ └── sub_pkg2│ ├── __init__.py│ └── module_Z.py└── main.py(入口文件)
module_X.py
from . import module_Yprint 'X __name__', __name__
module_Y.py
print 'Y __name__', __name__
當(dāng)我們直接運(yùn)行 python sub_pkg1/module_X.py 的時(shí)候,會(huì)報(bào)錯(cuò)
ValueError: Attempted relative import in non-package
當(dāng)我們這樣運(yùn)行的時(shí)候 python -m sub_pkg1.module_X , 才能正常運(yùn)行
Y __name__ sub_pkg1.moduleYX __name__ __main__
為什么會(huì)這樣?簡(jiǎn)單地說(shuō),直接運(yùn)行 .py 文件和 import 這個(gè)文件有很大區(qū)別。Python 解釋器判斷一個(gè) py 文件屬于哪個(gè) package 時(shí)并不完全由該文件所在的文件夾決定。它還取決于這個(gè)文件是如何 load 進(jìn)來(lái)的( 直接運(yùn)行 or import )。
有兩種方式加載一個(gè) py 文件:
·作為 top-level 腳本 作為 top-level 腳本指的是直接運(yùn)行腳本,比如 python xx.py。有且只能有一個(gè) top-level 腳本,就是最開(kāi)始執(zhí)行的那個(gè)(比如 python xx.py 中的 xx.py)?!ぷ鳛?module 作為 module 是指,執(zhí)行 python -m xx,或者在其它 py 文件中用 import 語(yǔ)句來(lái)加載,那么它就會(huì)被當(dāng)作一個(gè) module。
當(dāng)一個(gè) py 文件被加載之后,它會(huì)被賦予一個(gè)名字,保存在 __name__ 屬性中。如果是 top-level 腳本,那么名字就是 __main__ 。如果是作為 module,名字就是把它所在的 packages/subpackages 和文件名用 . 連接起來(lái)。
例如,moduleX 被 import 進(jìn)來(lái),它的名字就是
package.subpackage1.moduleX。如果 import 了 moduleA,它的名字是 package.moduleA。如果直接運(yùn)行 moduleX 或 moduleA,那么名字就都是 __main__ 了。
所以上面的 module_X 的 __name__ 是 __main__ , 因?yàn)樗侵苯舆\(yùn)行的, module_Y 的 __name__ 是 sub_pkg1.module_Y ,因?yàn)樗潜籭mport 來(lái)使用的。
module_X.py
# module_X導(dǎo)入module_Y# 相對(duì)導(dǎo)入from . import module_Y# 絕對(duì)導(dǎo)入from package.sub_pkg1 import module_Y
module_X.py
# module_X導(dǎo)入module_Z# 相對(duì)導(dǎo)入from ..sub_pkg2 import module_Z# 絕對(duì)導(dǎo)入from package.sub_pkg2 import module_Z
main.py
# main.py導(dǎo)入module_Yfrom package.sub_pkg1 import module_Y
特別需要注意的是,雖然上述模塊導(dǎo)入路徑是對(duì)的,除了 main.py 之外,都不可以通過(guò) python xx.py 的方式運(yùn)行,而是通過(guò)前面討論過(guò)個(gè) python -m 方式運(yùn)行。
相對(duì)導(dǎo)入可以避免硬編碼,對(duì)于包的維護(hù)是友好的。其缺點(diǎn)是可讀性較差,讓人很難清楚地了解到資源所在的位置。
絕對(duì)路徑導(dǎo)入由于其直觀往往是大家的首選。只要看一下導(dǎo)入語(yǔ)句你就能知道資源是從什么位置導(dǎo)入的。再者,就算當(dāng)前import語(yǔ)句的位置發(fā)生了變化,此絕對(duì)路徑導(dǎo)入的資源依然有效。實(shí)際上,官方也推薦使用絕對(duì)路徑導(dǎo)入。
《Python之禪》中提到:
明確 由于 隱晦
縱觀一些優(yōu)秀的開(kāi)源項(xiàng)目,絕對(duì)導(dǎo)入的使用也是更為普遍的。我個(gè)人通常以絕對(duì)導(dǎo)入為主,相對(duì)導(dǎo)入為輔,只會(huì)在同一個(gè)模塊層級(jí)中使用一些相對(duì)導(dǎo)入,如: from .fool import spam ,很少會(huì)使用 from ...fool import spam 這樣讓人迷惑的相對(duì)導(dǎo)入。
1.區(qū)分是文件夾還是包(有無(wú) __init__.py )?
2.非入口文件是否是使用python -m運(yùn)行的?
3.入口文件的層級(jí),是否高于任何包或者模塊?
聯(lián)系客服