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機制。