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