午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

Python Import系統(tǒng)給粗心人設(shè)下的“陷阱”

 River_LaLaLa 2016-08-22

Pythonimport系統(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ú)立這樣的能力。


這個(gè)布局令人驚訝的是所有的下列調(diào)用test_foo.py的方法可能都不起作用,因?yàn)閷?dǎo)入機(jī)制壞掉了(或者說(shuō)不能通過(guò)像這樣import example.foo或者from example import foo找到example,或者不能在一個(gè)非包的或者頂級(jí)包以上的包中像from .. import foo一樣的相對(duì)引用,或者如果一些其他的子模塊碰巧覆蓋了測(cè)試所用的頂級(jí)包的名稱,例如一個(gè)負(fù)責(zé)序列化的example.json模塊或者一個(gè)測(cè)試機(jī)模塊example.tests.unittest,一些更加難以理解的錯(cuò)誤可能會(huì)發(fā)生):


沒(méi)錯(cuò),如果你嘗試那個(gè)長(zhǎng)長(zhǎng)的包含所有調(diào)用方法的列表,它很可能損壞了
,并且如果你既對(duì)Python導(dǎo)入系統(tǒng)工作的方式不夠熟悉,又對(duì)它如何初始化不夠熟悉,那么錯(cuò)誤消息就沒(méi)有任何意義。(注意到如果工程僅僅顯式地使用了包內(nèi)引用的相對(duì)導(dǎo)入,則上面的最后兩條命令可能在Python 3.3以及未來(lái)版本中可以工作。任何引用一個(gè)頂級(jí)包并進(jìn)行絕對(duì)導(dǎo)入的做法將仍然會(huì)出錯(cuò)。

長(zhǎng)期以來(lái),用這種啟動(dòng)方式唯一能讓sys.path正確的方法是或者在test_foo.py中手動(dòng)設(shè)置(很少有Python的新手,甚至許多老手都不知道怎么做)或者確保導(dǎo)入模塊而不是直接執(zhí)行它:


然而,從Python 2.6之后,下面命令仍然可以正常工作:


這最后一種方式就是我在用Python編程時(shí)喜歡使用的shell命令——離開(kāi)我的工作目錄,設(shè)置到工程目錄,然后使用-m開(kāi)關(guān)來(lái)執(zhí)行相關(guān)的子模塊,例如測(cè)試或者命令行工具。如果我需要在不同的目錄下工作,好的,這就是為什么我喜歡開(kāi)著多個(gè)shell會(huì)話。

當(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.pyIDLE中打開(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)于Pythonsocket模塊,你可能傾向于命名你的實(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é)碼文件陷阱

緊跟著之前小節(jié)的例子之后,假設(shè)我們決定通過(guò)重命名文件來(lái)修復(fù)我們錯(cuò)誤的腳本名。在Python 2中,我們會(huì)發(fā)現(xiàn)這仍然不起作用:


很明顯,一些奇怪的事情發(fā)生了,因?yàn)槲覀兛吹皆阱e(cuò)誤追蹤中顯示一個(gè)注釋行出了問(wèn)題。事實(shí)上,由我們之前錯(cuò)誤的導(dǎo)入動(dòng)作而緩存的字節(jié)碼文件仍然存在,并且導(dǎo)致了這一問(wèn)題,但是當(dāng)Python試圖在錯(cuò)誤追蹤中顯示出錯(cuò)的源代碼行時(shí),它轉(zhuǎn)而去標(biāo)準(zhǔn)庫(kù)模塊中尋找源代碼行。刪除舊的字節(jié)碼文件可以讓它正確運(yùn)行:


這一特殊的陷阱在Python 3.2以及之后的版本已經(jīng)徹底去除了。在這些版本中,解釋器可以區(qū)別獨(dú)立的字節(jié)碼文件(例如上文中的socket.pyc)和緩存的字節(jié)碼文件(存儲(chǔ)在自動(dòng)生成的__pycache__目錄下)。如果相應(yīng)的源文件不存在,后者會(huì)被解釋器忽略掉,所以上述的為源文件重命名如期工作了:


然而,需要注意的是,如果Python 2留下了一個(gè)獨(dú)立的字節(jié)碼文件,混用Python 2Python 3會(huì)導(dǎo)致問(wèn)題:


如果你不是某個(gè)Python實(shí)現(xiàn)的核心開(kāi)發(fā)人員,導(dǎo)入舊的字節(jié)碼的問(wèn)題很可能當(dāng)重命名Python源文件時(shí)發(fā)生。而對(duì)于Python實(shí)現(xiàn)的開(kāi)發(fā)人員,它則可能在我們使用負(fù)責(zé)首先產(chǎn)生字節(jié)碼的編譯器部件的任何時(shí)刻發(fā)生——這就是為什么CPythonMakefile中包含一個(gè)make pycremoval目標(biāo)。

子模塊被加入包命名空間的陷阱

許多人已經(jīng)體驗(yàn)過(guò)了在僅僅導(dǎo)入了子模塊所在的包而去使用該子模塊時(shí)存在的問(wèn)題了:


然而很少被人知的一件事是,當(dāng)一個(gè)子模塊在無(wú)論任何地方加載時(shí),它會(huì)被自動(dòng)地添加到包的全局命名空間中:


當(dāng)在一個(gè)__init__.py文件中導(dǎo)入或者定義一個(gè)同當(dāng)前包的子模塊擁有相同名字的值時(shí),結(jié)果可能會(huì)更讓你驚訝。如果子模塊在導(dǎo)入或者定義了相同名字的值之后的任意位置被任何模塊加載了,它將會(huì)在__init__.py的全局命名空間中覆蓋已經(jīng)導(dǎo)入的或者定義的名字。

更多的奇怪的陷阱

上面提到的都是一些平常的陷阱,但是還存在其他陷阱,尤其是如果你開(kāi)始著手于擴(kuò)展或者重寫(xiě)默認(rèn)import系統(tǒng)的工作時(shí)。

最后我希望對(duì)這些增加一些細(xì)節(jié)描述:

  •  __import__的怪異的簽名

  • 模塊全局變量(__import__, __path__, __package__)的影響

  • 3.3版本之前線程的問(wèn)題

  • 3.3版本之前默認(rèn)的機(jī)器缺少對(duì)PEP 302的支持

  • 3.3版本之前命名空間包中的非合作的包的部分

  • sys.path[0]初始化變量

  • 關(guān)于pickle, multiprocessing和主模塊的問(wèn)題的更多內(nèi)容(參見(jiàn)PEP 395

  • __main__不總是一個(gè)頂級(jí)包(多虧了-m

  • 事實(shí)上,在導(dǎo)入過(guò)程中,模塊是允許在sys.modules中替換他們自己的

  • __file__可能不指代一個(gè)實(shí)際文件系統(tǒng)的位置

  • 自從3.2之后,你不能僅僅添加c或者o來(lái)獲得緩存的字節(jié)碼文件


英文原文:http://python-notes./en/latest/python_concepts/import_traps.html
譯者:LuCima


    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類(lèi)似文章 更多