Python的import系統(tǒng)是非常強(qiáng)大的,但是也非常復(fù)雜。直到Python 3.3版本的發(fā)布,都沒(méi)有關(guān)于之前預(yù)計(jì)的import語(yǔ)義的全面的解釋,甚至跟著3.3版本的發(fā)布,sys.path如何初始化的細(xì)節(jié)也仍然需要搞清楚。 即使3.3版本清除了許多東西,它仍舊需要搞定許多后向兼容性問(wèn)題,這些問(wèn)題可能導(dǎo)致一些奇怪的行為。并且,為了搞清一些第三方框架的運(yùn)行機(jī)制,我們也需要充分了解3.3版本。 此外,即使不使用任何導(dǎo)入系統(tǒng)中奇異的特性,在郵件列表或者像Stack Overflow一樣的Q&A網(wǎng)站中也經(jīng)常出現(xiàn)相當(dāng)多的常見(jiàn)的錯(cuò)誤。 這篇短文的內(nèi)容僅僅理論上向前包含至Python 2.6版本。大多數(shù)內(nèi)容也適合于早期版本,但我不會(huì)對(duì)2.6以前的版本細(xì)節(jié)給出任何解釋。 丟失的__init__.py陷阱 這個(gè)陷阱適用于2.x版本,也包括3.2及3.2之前的3.x版本。 在Python 3.3之前,文件系統(tǒng)目錄,以及zipfile中的目錄,必須包含一個(gè)__init__.py文件以使它被識(shí)別為Python的包目錄。即使當(dāng)包被導(dǎo)入時(shí)沒(méi)有初始化代碼要運(yùn)行,解釋器仍然需要一個(gè)空的__init__.py文件以便在那個(gè)目錄下能夠找到任何模塊或者子包。 這一情況在Python 3.3中改變了:現(xiàn)在任何一個(gè)在sys.path中的目錄,如果和要查找的包名稱一致,那么它將被視為該包的可以起作用的模塊或子包。 __init__.py的陷阱 這是一個(gè)在Python 3.3中增加的全新的”陷阱“,是由于修改之前的陷阱而帶來(lái)的:如果一個(gè)sys.path所導(dǎo)入的包的一個(gè)子目錄下也包含一個(gè)__init__.py文件,則Python解釋器會(huì)創(chuàng)建一個(gè)僅僅包含來(lái)自于該目錄下的單目錄包,而不是像之前一節(jié)描述的一樣,去尋找所有具有相同名稱的子目錄。 即使在sys.path中存在其他的不包括__init__.py文件子目錄但是和要找的包名稱相同,問(wèn)題也同樣會(huì)發(fā)生。 這一復(fù)雜情況是由于后向兼容性限制而強(qiáng)加于我們的——如果沒(méi)有這個(gè)問(wèn)題,當(dāng)Python 3.3讓用戶可選是否在包中需要?jiǎng)?chuàng)建__init__.py文件的時(shí)候,一些現(xiàn)存的代碼可能會(huì)崩潰。 然而,這一點(diǎn)也是很有用的,因?yàn)樗沟蔑@式地聲明一個(gè)包已經(jīng)完成,不再接受額外貢獻(xiàn)代碼變得可能。所有的標(biāo)準(zhǔn)庫(kù)目前都是這樣工作的,雖然一些包可能會(huì)開(kāi)放它們的命名空間來(lái)在未來(lái)版本中接受第三方的貢獻(xiàn)代碼(特別的,encodings包將確定在Python 3.4時(shí)開(kāi)放)。 雙重引用陷阱 緊接著的這個(gè)陷阱存在于目前所有的Python版本中,包括Python 3.3,并且可以用下面一句話總結(jié):“永遠(yuǎn)不要直接向Python路徑中添加一個(gè)包目錄,或者包內(nèi)的任何目錄”。 這樣做的原因是在那個(gè)目錄下的每一個(gè)模塊現(xiàn)在都潛在地有兩個(gè)不同的可以訪問(wèn)的名字:作為頂級(jí)模塊(由于目錄在sys.path中)以及作為包的子模塊(如果高一級(jí)的包含包本身的目錄也在sys.path中)。 舉個(gè)例子,Django(直到并包括1.3版本)在為特定站點(diǎn)創(chuàng)建應(yīng)用時(shí)的做法是錯(cuò)誤的——這個(gè)應(yīng)用最后可以在模塊命名空間中被作為app以及site.app來(lái)接入,并且事實(shí)上存在兩份不同的模塊的副本。如果有任何有意義的可變的模塊級(jí)的狀態(tài),上述情況會(huì)導(dǎo)致困惑,所以這一行為從1.4版本中默認(rèn)的文件夾結(jié)構(gòu)中移除了(特定站點(diǎn)的應(yīng)用將一直需要像Django版本說(shuō)明中敘述的一樣,完全匹配站點(diǎn)名稱才可以)。 不幸的是,這仍然是十分容易違反的規(guī)則,因?yàn)槿绻阍噲D從命令行通過(guò)文件名而不是使用-m開(kāi)關(guān)去運(yùn)行一個(gè)包內(nèi)的模塊,它就會(huì)自動(dòng)發(fā)生。 考慮一個(gè)簡(jiǎn)單的包,其布局如下(我在我自己的工程里專門(mén)沿著這幾條線使用了這樣的包布局——許多人討厭在像這樣的包目錄里做嵌套測(cè)試,而喜歡平行的結(jié)構(gòu),但是我更喜歡使用顯式的相對(duì)的導(dǎo)入方式來(lái)保證模塊測(cè)試與包名稱獨(dú)立這樣的能力。
長(zhǎng)期以來(lái),用這種啟動(dòng)方式唯一能讓sys.path正確的方法是或者在test_foo.py中手動(dòng)設(shè)置(很少有Python的新手,甚至許多老手都不知道怎么做)或者確保導(dǎo)入模塊而不是直接執(zhí)行它:
當(dāng)我正在使用一個(gè)嵌入式測(cè)試用例作為例子時(shí),當(dāng)你為了確保sys.path正確初始化了而沒(méi)有在父目錄使用-m開(kāi)關(guān)去在包中直接執(zhí)行一個(gè)腳本時(shí),類(lèi)似的問(wèn)題隨時(shí)都會(huì)發(fā)生(例如1.4版本之前的Django工程布局會(huì)在當(dāng)從包內(nèi)運(yùn)行manage.py時(shí)產(chǎn)生問(wèn)題,它會(huì)將包目錄放入sys.path以致導(dǎo)致這個(gè)雙重導(dǎo)入問(wèn)題——1.4版本之后的布局通過(guò)把manage.py移到包目錄外面而解決了這一問(wèn)題)。 事實(shí)是大多數(shù)從命令行調(diào)用Python代碼在當(dāng)代碼位于一個(gè)包內(nèi)時(shí)都會(huì)崩潰,而兩個(gè)可以工作的方式又對(duì)當(dāng)前工作目錄非常敏感,這對(duì)于新手來(lái)說(shuō)非常困惑。我個(gè)人相信這是導(dǎo)致Python包復(fù)雜并且很難被正確使用這一觀點(diǎn)的關(guān)鍵因素。 這個(gè)問(wèn)題甚至不限于命令行——如果test_foo.py在IDLE中打開(kāi)并且你試圖通過(guò)F5運(yùn)行它時(shí),或者你試圖在一個(gè)圖像化的文件瀏覽器中通過(guò)點(diǎn)擊它來(lái)運(yùn)行時(shí),它就會(huì)像通過(guò)命令行直接運(yùn)行一樣失敗。 在sys.path中不要寫(xiě)包目錄這一規(guī)則的存在有一個(gè)原因,即解釋器當(dāng)確定sys.path[0]是所有錯(cuò)誤的根源時(shí)它自己也不會(huì)參照這一條規(guī)則。 然而,即使在未來(lái)版本的Python中在這個(gè)部分有許多改善(參見(jiàn)PEP 395),這個(gè)陷阱也會(huì)在所有當(dāng)前版本中存在。 執(zhí)行主模塊兩次 這是上述雙重引用問(wèn)題的一個(gè)變種,它不需要任何錯(cuò)誤的sys.path條目。 對(duì)于當(dāng)主模塊也被作為普通模塊導(dǎo)入的情形來(lái)說(shuō)非常特別,實(shí)際上它會(huì)產(chǎn)生同一個(gè)模塊的兩個(gè)不同名稱的實(shí)例。 正如任何雙重導(dǎo)入問(wèn)題,如果存儲(chǔ)在__main__中的狀態(tài)對(duì)于程序正確運(yùn)行十分重要,或者在主模塊中有一些頂級(jí)代碼執(zhí)行了不止一次會(huì)產(chǎn)生未知的副作用,之后,這個(gè)復(fù)制品也會(huì)產(chǎn)生復(fù)雜的意想不到的錯(cuò)誤。 這僅僅是為什么在更加復(fù)雜的應(yīng)用中主模塊需要保持代碼最少的一個(gè)原因——通常將大多數(shù)的功能移到在單獨(dú)的模塊里的一個(gè)函數(shù)或者一個(gè)對(duì)象中并在主模塊中導(dǎo)入該模塊會(huì)更加魯棒。那樣,不經(jīng)意得執(zhí)行主模塊兩次將變得沒(méi)有害處。保證主模塊精簡(jiǎn)也可以避免伴隨著對(duì)象序列化以及多線程包的一些潛在的問(wèn)題。 命名覆蓋陷阱 另一個(gè)常見(jiàn)的陷阱,特別對(duì)于初學(xué)者來(lái)說(shuō),是使用一個(gè)本地模塊名導(dǎo)致覆蓋了程序所依賴的標(biāo)準(zhǔn)庫(kù)的或是第三方的包或者模塊。一個(gè)特別意想不到的碰到這個(gè)陷阱的情況是對(duì)一個(gè)腳本使用這樣的名字,因?yàn)檫@會(huì)結(jié)合之前“執(zhí)行主模塊兩次”陷阱導(dǎo)致問(wèn)題。例如,如果嘗試學(xué)習(xí)更多關(guān)于Python的socket模塊,你可能傾向于命名你的實(shí)驗(yàn)?zāi)_本為socket.py。事實(shí)證明這是一個(gè)壞主意,因?yàn)槭褂眠@樣的名字意味著Python解釋器可以不再去標(biāo)準(zhǔn)庫(kù)中尋找真正的socket模塊,因?yàn)楫?dāng)前目錄里的這個(gè)socket模塊擋住了去路:
緊跟著之前小節(jié)的例子之后,假設(shè)我們決定通過(guò)重命名文件來(lái)修復(fù)我們錯(cuò)誤的腳本名。在Python 2中,我們會(huì)發(fā)現(xiàn)這仍然不起作用:
子模塊被加入包命名空間的陷阱 許多人已經(jīng)體驗(yàn)過(guò)了在僅僅導(dǎo)入了子模塊所在的包而去使用該子模塊時(shí)存在的問(wèn)題了:
更多的奇怪的陷阱 上面提到的都是一些平常的陷阱,但是還存在其他陷阱,尤其是如果你開(kāi)始著手于擴(kuò)展或者重寫(xiě)默認(rèn)import系統(tǒng)的工作時(shí)。 最后我希望對(duì)這些增加一些細(xì)節(jié)描述:
英文原文:http://python-notes./en/latest/python_concepts/import_traps.html
|
|
來(lái)自: River_LaLaLa > 《Python》