python多級目錄import_Python 中 import機制的一些問題
技術標籤:python多級目錄import
sys.path 和 Working directory 是不同的
1. Working Directory 工作目錄
working directory 是在程式中通過相對路徑訪問檔案的起始點,是作業系統的概念,所有程式都會涉及到,在python中你可以通過下面的程式碼得到:
os.getcwd() # get current working directory
它可能會影響你開啟、儲存檔案, 只要不在程式中修改,在哪裡開啟的程式,工作目錄(working directory) 就在哪裡。
舉個例子:
對於同一個檔案xxx.py, 分別從它所在目錄執行和在上一級目錄執行, 工作目錄
$ python xxx.py # 在xxx.py 所在目錄執行
$ cd ..
$ python project/xxx.py # 在xxx.py 上一級,目錄執行
這裡python xxx.py
與 python project/xxx.py
雖然執行的是同一個目錄下同一個程式,但是工作目錄卻是不一樣的,一個在xxx.py
所在目錄,另一個在上一級目錄。
這和c語言類似:
./a.out
與 ./project/a.out
工作目錄是不一樣的。
我們在程式裡面通過相對路徑訪問一個資源,都是依賴於這個執行時才確定的工作目錄的。需要注意的是這是一個執行時的變數,它是整個程式全域性共享的,不受程式碼檔案在哪個子目錄的影響。
初學者可能會誤以為project/module1/a.py
中寫的程式碼與 project/module2/a.py
中寫的程式碼 工作目錄不同,分別呼叫open('xx.txt')
會開啟各自目錄下的xx.txt
。但這是錯誤的看法,在程式執行後他們會共享一個工作目錄,開啟同一個檔案,而這個檔案的具體位置由執行時呼叫的路徑確定。
2. 不是環境變數的PATH:sys.path
sys.path 也是程式執行時所有模組共享的, 它表示是import 查詢的路徑, 你可能會認為 sys.path 與working directory 是一樣的,但其實不是,sys.path 是由開始執行的檔案(入口檔案)位置決定的
python xxx.py
與 python project/xxx.py
工作目錄不同,但是sys.path卻相同,都是xxx.py
所在的位置。這樣的機制保證了import 不受執行路徑的影響, 是十分合理的設計。
但是,有些時候,我們會看到有人喜歡把這個開始執行的入口檔案放在子目錄中 (雖然我們並不建議這樣做,但是在程式debug的時候,我們希望單獨執行一個子目錄中的檔案,這種情況還是時有發生)
例如這個入口檔案在這裡:task/main.py
當我們執行
# 目錄結構示意圖
# project/
# ├── task
# │ ├── main.py
# │ └── ....
# ├── util
# │ ├── xx_util.py
# │ └── ....
# └── ....
$ python task/main.py
sys.path 就會進入到task 目錄中,這樣main.py 想要 import main 目錄外的模組就會出問題,例如import util.xx_util
就會出現 ModuleNotFoundError
, 就算本目錄下的檔案 import task.xxx
也會出現錯誤,因為sys.path 不對,在task目錄下就沒有那些檔案。
這種情況怎麼辦呢?我看到過幾種做法:
sys.path.append('..')
將上一級目錄 append 進來
非常不推薦這種做法,動態改變sys.path會使靜態分析工具失效,例如pycharm 等IDE的程式碼提示。這在大型專案中會極大降低程式碼的可閱讀性、增加開發難度這是軟體開發的災難
(補充:通過環境變數PYTHONPATH 指定目錄本質上也是sys.path 上append)- 使用相對路徑
import .task.xxx
,from .. import util
同樣非常不推薦這樣做,特別是我們只是想臨時debug一下子模組中的包的時候,修改好後還需要改回去,十分繁瑣,還有可能帶來不一致的問題這是軟體開發的災難 - [ 推薦做法 ] 使用
python -m task.main
執行程式
此時sys.path就在執行這行程式碼時所在的目錄,也就是working directory, 而不會進入main所在的位置,這是十分簡單的解決方法,不會帶來新的問題,但是我們卻看到許多工作大量使用`sys.path.append('..')
,importlib.import_module
之類的動態程式碼解決這類問題,這種動態性的修改會給軟體的維護帶來很多不必要的麻煩這是軟體開發的災難
Python和 Java的import機制不同點對比
- Python沒有Java不引用直接通過絕對路徑呼叫的方式
- Java 不能重新命名,Python 可以 import original_name as new_name
- Java不能 import package,而python可以import package,python package的內容在
__init__.py
中定義
可以認為python import 一個 package(一個資料夾)就是在import 那個資料夾下的__init__.py
,這個檔案在python2 中必須顯式提供,在python3中會預設有個空的。
需要注意的是如果__init__.py
中什麼都沒有,那麼import這個包是沒什麼用的,我們通常會在__init__.py
中 import這個包內的一些函式/類,這樣可以減少import的深度,用好它是提高程式碼可讀性的有力封裝工具 - Python import 後仍然要使用全名
import torch.nn.Conv
java:Conv.xxxx
python :torch.nn.Conv.xxxx
這種情況推薦重新命名
[順便說一下] Python import 和c/c++ #include 對比
簡單來說include 是把檔案直接拼接進來,再通過編譯器編譯,所以規範的c語言標頭檔案中不能出現函式/變數的定義,只能出現宣告,否則這個標頭檔案中定義的函式/變數就會因為被多個cpp/c檔案#inlcude而被編譯多次,這樣所有編譯好的中間檔案會在連結時出現重定義的錯誤。這個錯誤常被初學者誤解,認為加入標頭檔案保護就可以解決,例如:
#ifndef SOME_CLASS_H
#define SOME_CLASS_H
// user code
#endif
非常遺憾,加入標頭檔案保護也不能避免連結時出現的重定義錯誤(它只能避免編譯時的重定義,而不能避免連結時的),因為只要有多個檔案include這個標頭檔案,裡面的東西仍然會被多次編譯,最後在連結時出錯。
理解include機制是做c/c++程式開發必須的, 它告訴我們標頭檔案裡面什麼能寫,什麼不能寫。但篇幅有限,這裡就不展開講了,下一篇文章可以介紹一下include機制。