本文主要介紹如何在一個python項目中,優雅的實現項目內各個package的模塊(module)之間的相互引用。
之所以寫這篇文章,是因為網上流傳的各種奇技淫巧簡直五花八門(包括stackoverflow等知名社區),極易誤導對python的import機制不熟悉的人。比如我就曾一度因為找不到優雅的import方式,而認為python是一門愚蠢的語言。所以,我把近一上午的學習結果總結出來,希望大家不要誤入歧途。
本文參考了如下兩篇博客:
habnab關於python package的精彩總結:地址點我
Jean-Paul Calderone關於python項目結構的建議:地址點我
demo project
本文以一個demo project為例,來介紹python的包管理機制。
這個demo project我放到github上了:地址點我
其中,項目根目錄有三個文件夾:
- data:存放項目數據
- doc:存放項目文檔
- package:一個demo python package
其中,package中的文件結構如下圖:
python package
基礎知識
當你import的時候,python只會在sys.path這個變量(一個list,你可以print出來看)里面的路徑中找可能匹配的package和module。
而一個package跟一個普通文件夾的區別在於,package的文件夾中多了一個__init__.py文件。換句話說,如果你在某個文件夾中添加了一個__init__.py文件,則python就認為這個文件夾是一個package。
__init__.py文件可以是空的(也推薦者這么做),它只是告訴python當前文件夾是一個package。當然,也可以在里面添加一些代碼,這些代碼會在import這個包的時候運行。
所以,請確保你要import的文件所在的文件夾有__init__.py文件(除非它在sys.path中某個文件夾下)。
錯誤的import做法
如上述project中,如果你想讓subpackage2中的foo2來import subpackage1中的foo1,便會出現找不到subpackage1的情況。
目前網絡上大部分的做法都是通過sys.path.append(yourpath)之類的方法,將你需要import的module的目錄添加到sys.path中。或者,通過修改PYTHONPATH這個環境變量來將添加(跟修改sys.path效果相同)。
但是,這種做法有如下幾個缺點:
- 如果你用PYTHONPATH,那么當有多個項目時,你需要把每個項目的根目錄都加入到PYTHONPATH中,會使得PYTHONPATH變得十分臃腫
- 如果你使用sys.path,由於文件夾是動態添加的,所以當你使用相對路徑的時候,實際路徑會十分依賴於你的入口函數,當入口函數改變很可能就會導致代碼無法運行
- 如果你使用絕對路徑,將你的代碼在其他機器上運行的時候需要重新配置這些變量,十分麻煩
正確的做法
代碼中正常import
首先,在代碼中按照正常方式導入你需要的包
比如,你需要在app.py中導入foo1,則:
from package.subpackage1 import foo1
- 1
雖然你可能發現from subpackage1 import foo1也可以正常運行,但是請避免這種使用相對路徑的方法。因為這在python3中將不再支持,同時也有可能會引起奇怪的問題。同時,雖然PEP 328中也給出了 from .subpackage1 import foo1這樣的形式,但是還是不要自己給自己制造麻煩,統一使用完整路徑(絕對路徑)為好。
再比如,如果你需要在foo2.py中導入foo1.py(在不同的subpackage中),則:
from package.subpackage1 import foo1
- 1
跟上面一模一樣,這就是使用絕對路徑的好處,各處的引用高度統一。同時,如果你的package被安裝在其他用戶的機器中,其他用戶也會使用這種絕對路徑來import你package中的模塊(回想你自己import第三方package的情景)。
創建__main__.py文件
在package的根目錄中創建__main__.py文件,可以使得你的package可以通過python -m直接運行。
demo中的__main__.py文件十分簡單:
from package.app import main main()
- 1
- 2
即import真正的主函數app.py中的main方法,然后調用main()
用python -m運行你的python文件
python的-m參數官方說法是:
Searches sys.path for the named module and runs the corresponding .py file as a script.
在下面的例子中,加上-m參數后,所運行的.py文件便會識別其頂層的package
回到剛才的例子。創建完__main__.py之后,cd到項目的根目錄,運行
python -m package
- 1
即可實現直接運行__main__.py,即直接運行了package這個包
如果你想直接運行package內的某個.py文件,比如foo1,則:
python -m package.subpackage1.foo1
- 1
當然,你要確保foo1中存在判斷其是否是入口函數的邏輯,如下:
if __name__ == "__main__": speak()
- 1
- 2
總結
至此,我們已經實現了你所希望的所有功能:
- 在project內部實現各個模塊之間的import
- project中的各個.py文件可以直接運行
- 將project遷移到其他機器時,不用進行額外配置
如果還有不明白的,可以將github上的源碼下載下來看一看,然后用python -m運行一下