VS Code 透過 settings.json 設定 PYTHONPATH
告別 ModuleNotFoundError
前幾天,同事為專案的局部元件寫了一個偵錯小程式,我們姑且稱為 debugger.py
。該程式中會使用到整個專案的共同設定,這些設定則放在專案下的 configs
模組(資料夾)裡,所以需要另外 import:
import os
import sys
from configs.config import TestMode # import 本地的 configs 模組
而專案的結構則是(這裡只凸顯兩者的「 相對層級關係 」,其餘細節省略):
.
├── 元件
│ ├── debugger.py
│
├── configs
│ ├── config.py
│
有經驗的你可能不用執行這個小程式就能預料 — 它找不到 configs
!
不出所料,直接執行之後,會出現錯誤訊息:
這就牽涉到,Python 直譯器在 import 時,究竟「 如何尋找 import 路徑 」議題。
Python import 基礎
可先參考下列三篇文章,建立關於 import 的基礎世界觀:
- import雜談之一 — — — import路徑的相對論
- import雜談之二 — — — export機制以及namespace package
- import雜談之三 — — — sys.path的洪荒之時
從上述引用內容可知, debugger.py
所以會出現 ModuleNotFoundError
,正是因為 from configs.config import TestMode
這段程式碼,是希望從「專案根目錄」的角度出發去 import。
但在沒有額外設定的情況下,專案根目錄並不在 sys.path
裡--除非 debugger.py
就在專案根目錄底下(容後述)。所以自然就找不到 configs
模組。
兩個幫sys.path
「加料」的方法
因此,若想要正確執行 debugger.py
,我們需要把「專案根目錄」的路徑,加入到 sys.path
,此時有兩個常見方法。
一、在程式碼中手動加入sys.path
也就是在from configs.config...
之前,先手動加入這段sys.path.append('專案根目錄')
。
這樣的好處是,別人不需要額外設定PYTHONPATH
,因為程式已經幫我們做完了。
而缺點則是 — — 對我來說是一個缺點🐸 — — 太醜了。不僅會降低程式碼的可讀性,且Flake8
也會給出錯誤訊息(E402):
Module level import not at top of file
不過,在團隊協作時,這樣的做法可以減少大家在「設定未同步」時的潛在問題,所以還是要依不同情境考量,究竟要用哪種方式。
基於協作一致性考慮,本文的案例我仍建議同事採用這個做法。
二、使用PYTHONPATH
儘管如此,有時候我們只是個人開發,或不想在程式碼中直接修改 sys.path
,則設定 PYTHONPATH
是更常見的做法。
VS Code 設定PYTHONPATH
首先要說明的是,PYTHONPATH
在不同情境會被不同的工具使用,比如 Dockerfile。
而本文只集中在「使用 VS Code」的情境下 — — 究竟要怎麼設定,才能讓 VS Code 取得正確的路徑?
嚴格說,是指 VS Code 的整合命令列中,如何正確套用PYTHONPATH
。好讓 VS Code 在以整合命令列執行程式時,可以正確 import,而不會出現ModuleNotFoundError
。
通常這段設定會放在「專案」下的settings.json
,也就是「專案/.vscode/settings.json
」,你要使用「使用者」設定(全域的settings.json
)也是可以,只是要留意不同專案是否會因此而有不一致的結果。
設定PYTHONPATH
的三種方式
關於 VS Code 加入 PYTHONPATH
的 settings.json
設定,從以前(至少是 3 年前)到現在,有過方式變遷,我們只需要知道:舊的方法已經不管用。
第一種方式:直接設定 settings.json(已廢棄)
"python.pythonPath": "專案根目錄"
簡單暴力,透過 python.pythonPath
這個 token,直接幫 VS Code 直接指定 PYTHONPATH
,但此 token 已經作古(被移除)了,可以不必理會。
第二種方式:透過 env 檔
你先建立 env 檔,讓 VS Code 去讀取它。不用說,env 裡面必須要有 PYTHONPATH
這個 key 才行。
通常我們會這樣設定 env 內容:
PYTHONPATH=$(pwd)
或
PYTHONPATH=.
可以看出,使用的是「相對路徑」或「環境變數」,兩者異曲同工,實際想指向的,都是專案根目錄。不用絕對路徑,則是為了「方便在不同專案之間套用」。
要特別注意,無論是.
還是$(pwd)
,都是「相對於該 env 檔所在的資料夾」而言。
換句話說,如果你的 env「沒有」放在專案根目錄底下,這個設定就可能會出錯。此時為了避免過度複雜,改用絕對路徑也是可以的。
重要:sys.path 的第一順位值
需要補充說明的是,執行任意 Python 檔案時,Python 直譯器會依序嘗試 sys.path
裡的每個值,直到找到你要 import 的模組。而 sys.path
的「 第一順位值」就是 執行檔所在的路徑(資料夾) :
By default, the interpreter looks for a module within the current directory. To make the interpreter search in some other directory you just simply have to change the current directory.
換句話說,如果debugger.py
「正好」就在專案根目錄下,那就不需要額外的設定。因此,本文案例之所以會出現ModuleNotFoundError
,也正因debugger.py
不在專案的根目錄裡。
可想而知,把debugger.py
改放在專案根目錄下,就不會有問題了。但這並不是解決問題的根本方法,因為會讓專案結構變得「不整齊、缺乏一致性」。
settings.json 設定 env 讀取路徑
有了 env 檔案,接著就是要讓 VS Code 去讀取它, settings.json
要加入下列內容:
"python.envFile": "${workspaceFolder}/.env"
${workspaceFolder}
的意思是「VS Code 當前開啟的專案根目錄」。
${workspaceFolder}
的意思是「VS Code 當前開啟的專案根目錄」。
你可以把 env 檔放在更深層的資料夾,只要python.envFile
的路徑正確,VS Code 仍然能夠正確讀取它。
但如前所述,env 中PYTHONOPATH
的值若為$(pwd)
或.
,那它的「實際路徑」就會隨著 env 所在的路徑而變動,有著不確定性。
所以,我們往往就是把 env 放在專案根目錄。當然,它通常叫「.env
」。
第三種方式:「整合 terminal」設定
一開始看到ModuleNotFoundError
時,我立刻想到的就是第二種設定,趕緊翻出筆記,依樣畫葫蘆。
沒想到,它不 work!
我開啟 VS Code 的整合命令列去執行debugger.py
,發現它依舊找不到configs
的路徑。這就奇了,我之前都是這樣做的啊?
後來想了一下,以前發生問題,主要是 VS Code 在對程式進行靜態分析時,會直接提示找不到路徑,所以我才透過python.envFile
和 env 檔來解決。
而且這次是整合命令列的執行問題,兩者不盡相同。
但無妨,反正問題的本質不變,我們知道其中關鍵,就是PYTHONPATH
設定,所以關鍵字打下去,不意外的——答案就在檔案裡!
官方文件
使用「vscode pythonpath」關鍵字,你將輕易找到這篇「Using Python environments in VS Code」。
其中,和本文議題直接相關的,就是最下方的「Use of the PYTHONPATH variable」部分:
The PYTHONPATH environment variable specifies additional locations where the Python interpreter should look for modules. In VS Code, PYTHONPATH can be set through the terminal settings (
terminal.integrated.env.*
) and/or within an.env
file.
本段的最大重點就是:PYTHONPATH
環境變數除了使用 env 檔外,也可以直接由terminal.integrated
這個 token 設定。
看到這裡,整個「故事背景」我們已經瞭解得差不多了,直接來看設定吧!為 VS Code 的整合命令列設定PYTHONPATH
為 VS Code 的整合命令列設定PYTHONPATH
設定內容:
"terminal.integrated.env.linux": {
"PYTHONPATH": "${workspaceFolder}"
},
這個設定允許你,為「VS Code 整合命令列」額外新增環境變數,方便你在命令列中執行程式。
上述的env.linux
可以改為.osx
或.windows
,讓你依不同作業系統,分別設定不同變數內容。如果其實都一致,就可以像檔案中那樣,使用萬字元*
即可。
因為我只在 Linux VM 上執行,所以只設定了.linux
。
雖然檔案表明,你也可以透過 env 設定PYTHONPATH
,但 env 中的PYTHONPATH
,應該只對 linter、formatter 等工具有效,而不會影響 VS Code 的整合命令列。至少我無法透過 env 來解決命令列的執行問題。
特別提醒
terminal.integrated
這個設定如它的名稱所述, 只對 VS Code 的整合命令列有效:
However, in this case when the extension is performing an action that isn’t routed through the terminal, such as the use of a linter or formatter, then this setting won’t have an effect on module look-up.
舉例而言,如果你自行開一個 terminal 去執行 debugger.py
,則還是一樣會出現 ModuleNotFoundError
。
如果這對你造成困擾,那麼採用「在程式碼加入 sys.path
」的方式,或許才是適合的選擇。
Originally published at https://blog.kyomind.tw on February 11, 2023.