
1. ModuleNotFoundError: No module named yagmail 到底在告訴你什么1.1 這是Python最誠實(shí)的一類報(bào)錯(cuò)如果你在腳本第一行寫了import yagmail然后運(yùn)行終端卻甩回一行紅色日志ModuleNotFoundError: No module named yagmail別急著懷疑人生這一行信息量其實(shí)非常大。它的直譯是Python解釋器在當(dāng)前環(huán)境中按照它自己的模塊搜索路徑也就是sys.path找了半天沒有找到名字叫yagmail的包。注意這不屬于語法錯(cuò)誤也不屬于邏輯錯(cuò)誤而是導(dǎo)入期錯(cuò)誤。Python先把你的代碼做語法分析沒問題之后開始逐行執(zhí)行等真正跑到import yagmail這句話時(shí)才會去sys.path里列出的所有目錄里找包。全部找完還是沒有于是拋出一個(gè)ModuleNotFoundError。用生活化的話說就像你去廚房做菜菜譜第一步寫著“拿出生抽”你打開調(diào)料柜、臺面、冰箱都翻了一遍結(jié)果沒有于是你跟家里人喊了一嗓子“家里沒有生抽”。Python就是這個(gè)“喊話”機(jī)制它只會按固定目錄列表找不會自己跑到硬盤每個(gè)角落去全局搜索。所以當(dāng)你看到這種報(bào)錯(cuò)第一反應(yīng)應(yīng)該是不是腳本內(nèi)容有問題而是運(yùn)行環(huán)境里少了東西或者運(yùn)行環(huán)境和我以為的環(huán)境不是同一個(gè)。1.2 No module named 和 ImportError 不是一回事很多在早期Python版本里折騰過的朋友可能記得以前 import 失敗統(tǒng)一報(bào)ImportError沒有ModuleNotFoundError這一說。后來 Python 3.6 把“模塊不存在”單獨(dú)拆成了ModuleNotFoundError并且讓它成為ImportError的子類。這意味著兩件事第一如果你以前見過ImportError: No module named yagmail那和現(xiàn)在看到的ModuleNotFoundError: No module named yagmail本質(zhì)上是同一個(gè)問題只是版本不一樣Python的報(bào)錯(cuò)分類更細(xì)分了。第二有些老代碼為了兼容會用except ImportError去捕獲導(dǎo)入異常這依然能捕獲到ModuleNotFoundError因?yàn)樽宇悤桓割惍惓2东@。但在排查Bug時(shí)看到ModuleNotFoundError要更聚焦它明確告訴你“這個(gè)包根本不存在”而不是“這個(gè)包存在但內(nèi)部某一步導(dǎo)錯(cuò)了”那是另一個(gè)報(bào)錯(cuò)方向。從實(shí)操角度講遇到No module named yagmail默認(rèn)先判定“沒裝”但緊接著要意識到一個(gè)更扎心的問題裝了也可能找不到。因?yàn)檫@里有“環(huán)境”兩個(gè)字在中間作梗。這也就是為什么會出現(xiàn)“我明明pip install了為什么還是報(bào)錯(cuò)”這種讓人抓狂的情況。2. 為什么你已經(jīng) pip install 了程序還是找不到2.1 虛擬環(huán)境和全局環(huán)境的錯(cuò)位新手最容易掉進(jìn)去的坑就是環(huán)境錯(cuò)位。我給你描述一個(gè)極其常見的操作路徑你在 PyCharm 里新建了一個(gè)項(xiàng)目PyCharm 默認(rèn)會自動創(chuàng)建venv虛擬環(huán)境。好項(xiàng)目右邊的終端里pip install yagmail執(zhí)行成功沒有報(bào)錯(cuò)。接著你開心地去系統(tǒng)桌面打開 cmd 或者 Windows Terminal在另一個(gè)目錄運(yùn)行python test.py結(jié)果彈出ModuleNotFoundError: No module named yagmail。問題出在哪PyCharm 的項(xiàng)目終端里默認(rèn)已經(jīng)把虛擬環(huán)境激活了你安裝yagmail是裝進(jìn)了venv的site-packages。但你在系統(tǒng)終端里運(yùn)行的python是全局的 Python 解釋器。全局環(huán)境的第三方包目錄里沒有yagmail自然而然就報(bào)錯(cuò)了。所以在排查之前你要先分清“代碼是在哪個(gè)環(huán)境運(yùn)行的”。想驗(yàn)證當(dāng)前 Python 到底是哪一套可以運(yùn)行python -c import sys; print(sys.executable)如果在虛擬環(huán)境里打印出來的路徑會指向項(xiàng)目目錄下的venv/bin/pythonWindows 下是venv\Scripts\python.exe。如果執(zhí)行結(jié)果指向/usr/bin/python或者某個(gè)不知名路徑那就說明你根本沒在項(xiàng)目環(huán)境里跑腳本。我見過太多人刪了好幾次包、重裝好幾遍yagmail還是報(bào)錯(cuò)最后發(fā)現(xiàn)只是終端窗口用錯(cuò)了。建議是以后跑 Python 腳本盡量從同一套集成開發(fā)環(huán)境的終端窗口去跑比如 PyCharm 的項(xiàng)目終端、VS Code 的集成終端因?yàn)檫@些終端通常會幫你自動激活已選中的環(huán)境。不建議開一個(gè)獨(dú)立的系統(tǒng) cmd 去跑工程里的腳本那不是不行而是你很容易忘記激活環(huán)境。2.2 pip 和 python 可能根本不是同一套工具第二個(gè)高頻原因是pip對應(yīng)的 Python 環(huán)境和你的python命令對應(yīng)的 Python 環(huán)境不是同一個(gè)。現(xiàn)在很多電腦里同時(shí)裝了好幾個(gè) Python官方 python.org 的安裝包、Anaconda、MiniConda、Windows Store 里裝的 Python、還有各種 IDE 自帶的 Python。它們各自有獨(dú)立的site-packages目錄。如果某個(gè)程序在 PATH 里搶占了先機(jī)你在命令行敲pip時(shí)它指向的可能是一套環(huán)境敲python時(shí)它又指向了另一套環(huán)境。解決方案極其簡單但極其重要不要使用裸的pip命令改用python -m pip方式。python -m pip install yagmail這條命令的意思是把pip當(dāng)作python這個(gè)解釋器的一個(gè)模塊來運(yùn)行這樣就保證了 pip 一定跟隨當(dāng)前這個(gè) python 走裝進(jìn)當(dāng)前這個(gè) python 對應(yīng)的site-packages。只要運(yùn)行腳本時(shí)用的也是同一個(gè)python就不會發(fā)生“裝到 A 環(huán)境、跑在 B 環(huán)境”的詭異事件。這條經(jīng)驗(yàn)不只是針對yagmail。你以后遇到No module named numpy、No module named opencv、No module named pkg_resources甚至No module named pip排查第一步永遠(yuǎn)是問自己當(dāng)前正在運(yùn)行的 Python 到底是哪一套包又裝到哪一套去了把這兩個(gè)問題弄清楚至少能解決一半以上的導(dǎo)入類報(bào)錯(cuò)。2.3 系統(tǒng)Python保護(hù)機(jī)制externally-managed-environment近幾年在 Debian、Ubuntu 等 Linux 發(fā)行版上直接給系統(tǒng) Pythonpip install經(jīng)常會看到這樣的報(bào)錯(cuò)error: externally-managed-environment標(biāo)題相關(guān)熱詞里出現(xiàn)的pip install modelscope error: externally-managed-environment就是這個(gè)問題。很多人第一次見到這個(gè)錯(cuò)誤以為電腦壞了其實(shí)不是。這是系統(tǒng)在保護(hù)自己背后是 PEP 668 規(guī)范。系統(tǒng) Python 的第三方包主要由apt包管理器來維護(hù)如果你拿pip亂裝一通很容易覆蓋系統(tǒng)關(guān)鍵依賴甚至把整個(gè)python3環(huán)境搞崩。遇到這種報(bào)錯(cuò)我最推薦的做法是在項(xiàng)目目錄下創(chuàng)建一個(gè)虛擬環(huán)境在venv里安裝不要強(qiáng)行和系統(tǒng)環(huán)境對著干。如果只是臨時(shí)驗(yàn)證一個(gè)小模塊也可以使用--break-system-packages參數(shù)強(qiáng)制安裝但我不建議把這個(gè)養(yǎng)成長期習(xí)慣。你要是后面發(fā)現(xiàn)了依賴沖突會非常痛苦。3. 一步步把 yagmail 裝進(jìn)正確的環(huán)境3.1 安裝前先做一次環(huán)境體檢不要一上來就復(fù)制粘貼pip install yagmail我先建議你花 20 秒做環(huán)境體檢。按順序執(zhí)行下面幾條命令python --version python -m pip --version python -m pip show yagmail第一條看 Python 版本第三條看當(dāng)前環(huán)境里是否已經(jīng)存在yagmail。如果最后一條命令輸出了Name: yagmail、Version: 0.6.3、Location: ...這樣的信息說明當(dāng)前這個(gè) Python 環(huán)境里早就裝好了那你應(yīng)該回頭檢查之前腳本運(yùn)行用的到底是哪個(gè)環(huán)境。如果pip show沒有輸出任何信息只是安靜地回到命令提示符就說明確實(shí)沒有安裝。如果你創(chuàng)建了虛擬環(huán)境但還沒激活Windows 下用venv\Scripts\activateLinux 和 macOS 下用source venv/bin/activate激活后命令行的最前面一般會出現(xiàn)括號里寫著venv的提示符。這一步做好之后再繼續(xù)安裝。3.2 安裝命令與鏡像源選擇最基礎(chǔ)、最穩(wěn)的安裝命令是python -m pip install yagmail如果你在國內(nèi)的網(wǎng)絡(luò)環(huán)境里安裝可能會遇到下載慢、連接超時(shí)、卡在進(jìn)度條上不動的情況。這時(shí)候可以把包源切換到清華鏡像python -m pip install yagmail -i https://pypi.tuna.tsinghua.edu.cn/simple這個(gè)-i參數(shù)的意思是--index-url也就是臨時(shí)指定本次安裝使用的 PyPI 鏡像源。鏡像源的原理很簡單它相當(dāng)于把 PyPI 官方倉庫在海外服務(wù)器上的包同步了一份放在國內(nèi)訪問速度會明顯更快。需要注意的是鏡像源站的同步會有短暫延遲極少情況下會碰到某個(gè)包的最新版還沒同步過來這時(shí)把版本調(diào)低一級或者用官方源再試一次就好。如果你不是在一個(gè)全新的虛擬環(huán)境里安裝而是直接對系統(tǒng) Python 操作還可能會看到這么一行提示Defaulting to user installation because normal site-packages is not writeable翻譯一下就是當(dāng)前site-packages目錄不可寫pip 決定把包裝到當(dāng)前用戶目錄下。這種情況通常出現(xiàn)在 macOS 和 Linux 上。你確實(shí)可以接著用但我還是會建議優(yōu)先進(jìn)入一個(gè)虛擬環(huán)境再裝省得后續(xù)權(quán)限問題反復(fù)出現(xiàn)。3.3 安裝后真的要驗(yàn)證一次很多人在終端看到Successfully installed yagmail-0.6.3就以為萬事大吉了轉(zhuǎn)頭運(yùn)行腳本又報(bào)錯(cuò)。為什么因?yàn)閜ip install成功只能說明包裝進(jìn)了當(dāng)前pip對應(yīng)的環(huán)境但不代表你運(yùn)行腳本時(shí)使用的解釋器環(huán)境一定就是剛才那個(gè)。所以安裝完成之后我建議在同一個(gè)終端里運(yùn)行一次動態(tài)導(dǎo)入python -c import yagmail; print(yagmail.__version__)如果成功打印出版本號比如0.6.3那就說明當(dāng)前這個(gè)終端環(huán)境里真正可以用yagmail了。只要后續(xù)你始終用同一個(gè)環(huán)境去跑腳本就不會再看到No module named yagmail。記住一個(gè)原則pip show驗(yàn)證的是安裝元數(shù)據(jù)import驗(yàn)證的是真實(shí)的運(yùn)行時(shí)環(huán)境。把兩條命令都在同一個(gè)終端里跑一遍兩者一致才算徹底閉合。4. 安裝過程中那些反復(fù)把人勸退的問題4.1 老被提示 pip 版本太舊很多朋友在安裝任何包之前都會看到一行警告WARNING: You are using pip version 21.1.1; however, version 25.0.1 is available.這種提示看著很煩但說實(shí)話老版本的 pip 去安裝yagmail這種小包大概率也能成功。可如果一直拖延不升級遇到某些依賴解析復(fù)雜的包、或者對新版 wheel 二進(jìn)制包支持不佳的包時(shí)就容易卡住而且報(bào)錯(cuò)信息還很難讀懂。我的習(xí)慣是在新建虛擬環(huán)境并激活之后第一件事先升級 pippython -m pip install --upgrade pip等pip版本升上去以后再去install yagmail。你會發(fā)現(xiàn)后續(xù)的依賴解析更快關(guān)于externally-managed-environment之類的提示也會更準(zhǔn)確。升級完 pip 后再順手看看鏡像源如果之前裝的時(shí)候用的是默認(rèn)源升級也會更快。4.2 網(wǎng)絡(luò)超時(shí)、ReadTimeoutError、連接被重置在終端里遇到ReadTimeoutError或者Connection reset by peer之類的報(bào)錯(cuò)是網(wǎng)絡(luò)原因不是命令寫錯(cuò)了。默認(rèn)情況下pip 會從官方 PyPI 下載包如果你的網(wǎng)絡(luò)和官方源之間的連接不穩(wěn)定下載容易中斷。做法有三步一是加超時(shí)時(shí)間和重試次數(shù)python -m pip install yagmail --timeout 60 --retries 5二是直接用國內(nèi)鏡像源前面已經(jīng)說過用-i參數(shù)臨時(shí)指定即可。三是如果每次都很慢可以設(shè)置全局默認(rèn)鏡像源一勞永逸python -m pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple設(shè)置完之后默認(rèn)的 pip 下載行為就都走鏡像源了。值得注意的是如果你在公司內(nèi)網(wǎng)某些鏡像站可能訪問不了這時(shí)優(yōu)先用內(nèi)網(wǎng)自建的 PyPI 鏡像而不是硬套公共鏡像地址。4.3 依賴沖突和全局環(huán)境“大雜燴”yagmail 本身依賴很少通常只有python-dateutil之類的輕量庫所以單獨(dú)安裝它時(shí)幾乎遇不到依賴沖突。但如果你把所有 Python 包都塞進(jìn)同一個(gè)全局環(huán)境時(shí)間久了就會出問題。比如foo包依賴舊版urllib3bar包依賴新版urllib3兩個(gè)包裝在一起pip 在解析依賴時(shí)可能直接報(bào)出讓你看不懂的“Dependency conflict”錯(cuò)誤。這就要回到前面反復(fù)強(qiáng)調(diào)的虛擬環(huán)境。每一個(gè)項(xiàng)目都應(yīng)該有自己獨(dú)立的虛擬環(huán)境而不是一個(gè)環(huán)境里住滿了所有項(xiàng)目的地盤。這就像你不會讓十個(gè)家庭共用一間廚房一樣各種調(diào)料和生活習(xí)慣混在一起遲早會打起來。實(shí)際工作中我見過為了解一個(gè)依賴沖突查了三天的人最后發(fā)現(xiàn)把項(xiàng)目單獨(dú)venv一下問題在十分鐘之內(nèi)就解決掉了。4.4 總覺得要 sudo 才能裝用sudo pip install是很多人踩過的最大的坑。你可能覺得系統(tǒng)提示Permission denied那就加sudo好了。但在 Linux 系統(tǒng)里sudo pip install會把包直接寫進(jìn)系統(tǒng)級 Python 的site-packages一旦裝錯(cuò)版本或卸載不當(dāng)很容易把系統(tǒng)依賴搞亂。更麻煩的是sudo安裝的包權(quán)限屬于 root你以后的普通用戶進(jìn)程想刪都刪不掉。正確的解法是如果你只是自己開發(fā)用就創(chuàng)建虛擬環(huán)境不需要 root 權(quán)限也不會弄臟系統(tǒng)。如果非要為系統(tǒng) Python 裝包用python -m pip install --user yagmail安裝到用戶目錄避免動系統(tǒng)全局目錄。把“少用 sudo 裝 Python 包”這條刻進(jìn)腦子里可以幫你避開無數(shù)麻煩。5. yagmail 到底能干什么以及真實(shí)使用體驗(yàn)5.1 一個(gè)最小可用的發(fā)郵件腳本在解決完環(huán)境問題之后也許你會好奇yagmail這個(gè)包到底解決什么問題。簡單說它是一個(gè)用來“讓 Python 發(fā)送郵件變得非常簡單”的庫。如果用 Python 標(biāo)準(zhǔn)庫smtplib去發(fā)郵件你需要手動處理 SMTP 連接、登錄、構(gòu)造郵件頭、構(gòu)造 MIME 文本、加附件等一堆操作少說也要幾十行代碼。而yagmail把大部分細(xì)節(jié)都包掉了代碼可以少到一個(gè)令人驚訝的程度import yagmail yag yagmail.SMTP(useryougmail.com, passwordyour_app_password) yag.send( toreceiverexample.com, subject測試郵件, contents這是用 yagmail 發(fā)送的正文內(nèi)容。 )就這么幾行一封郵件就發(fā)出去了。這就是yagmail存在的意義明明一個(gè)自動化任務(wù)只需要很小的代碼量你沒必要為了它手寫太復(fù)雜的底層邏輯。對于跑數(shù)據(jù)定時(shí)報(bào)表、發(fā)監(jiān)控告警、給用戶批量發(fā)通知這類腳本來說它的簡潔性非常有吸引力。5.2 使用中需要注意的細(xì)節(jié)第一個(gè)要注意的是密碼問題。上面示例代碼里的明文密碼我個(gè)人不推薦直接寫死在腳本里尤其當(dāng)腳本要提交到 git 倉庫時(shí)。至少也要從環(huán)境變量里讀取或者使用郵箱服務(wù)商提供的“應(yīng)用專用密碼”。像 Gmail 這類郵箱開啟兩步驗(yàn)證之后可以為某個(gè)應(yīng)用單獨(dú)生成一個(gè)隨機(jī)密碼比主密碼安全得多。第二個(gè)細(xì)節(jié)是自定義 SMTP 服務(wù)器。默認(rèn)情況下yagmail會根據(jù)你傳入的郵箱后綴自動推斷 SMTP 服務(wù)器比如 Gmail 就自動去smtp.gmail.com。如果你用的是公司企業(yè)郵箱后綴可能比較小眾自動推斷會失敗這時(shí)需要顯式傳入host參數(shù)yag yagmail.SMTP(useryoucompany.com, passwordyour_password, hostsmtp.company.com)第三個(gè)細(xì)節(jié)是附件路徑。寫contents[附件文件.pdf]時(shí)如果使用相對路徑腳本執(zhí)行時(shí)的工作目錄必須和文件實(shí)際路徑一致。為了減少不確定性盡量用絕對路徑或者先通過os.path.abspath把相對路徑轉(zhuǎn)成絕對路徑。第四個(gè)細(xì)節(jié)是關(guān)于送達(dá)率的現(xiàn)實(shí)認(rèn)識。腳本顯示發(fā)送成功只代表 SMTP 服務(wù)端接收了這個(gè)郵件請求不代表收件方一定會把它放進(jìn)收件箱。你的發(fā)信域名如果缺少 SPF、DKIM 等設(shè)置或者服務(wù)器 IP 信譽(yù)不好郵件很容易進(jìn)垃圾箱。這是任何郵件自動化腳本都無法完全繞開的現(xiàn)實(shí)問題。6. 以后遇到 ModuleNotFoundError可以直接查這張表6.1 報(bào)錯(cuò)現(xiàn)象與排查操作對照報(bào)錯(cuò)現(xiàn)象可能原因推薦操作ModuleNotFoundError: No module named yagmail當(dāng)前環(huán)境沒有安裝這個(gè)包用python -m pip install yagmail安裝安裝后仍報(bào)相同錯(cuò)誤運(yùn)行腳本的 Python 和安裝包的 Python 不是同一個(gè)環(huán)境檢查python -c import sys; print(sys.executable)externally-managed-environment系統(tǒng) Python 受 PEP 668 保護(hù)創(chuàng)建并激活虛擬環(huán)境后再安裝Defaulting to user installation because normal site-packages is not writeable當(dāng)前用戶對 site-packages 無寫權(quán)限優(yōu)先使用虛擬環(huán)境或使用--user裝用戶目錄WARNING: You are using pip version 21.1.1pip 版本太舊python -m pip install --upgrade pipReadTimeoutError或連接被重置網(wǎng)絡(luò)到默認(rèn)源不穩(wěn)定使用國內(nèi)鏡像源或增加--timeout和--retries某個(gè)import報(bào)錯(cuò)但包明明存在同名下其他包干擾或者工作目錄錯(cuò)位在正確環(huán)境里重裝并用python -c import xxx驗(yàn)證這張表看起來簡單但覆蓋了我這些年處理 Python 導(dǎo)入類報(bào)錯(cuò)時(shí)遇到的大多數(shù)情況??梢哉f光是把“python -m pip”和“確認(rèn)當(dāng)前解釋器路徑”這兩個(gè)習(xí)慣養(yǎng)成市面上 70% 的ModuleNotFoundError都影響不到你。6.2 我處理這類問題時(shí)的幾條個(gè)人習(xí)慣最后把我自己的一套操作流程分享出來。新建一個(gè) Python 項(xiàng)目時(shí)我永遠(yuǎn)先執(zhí)行python -m venv venv然后激活虛擬環(huán)境。激活完先看一眼提示符前有沒有環(huán)境名再順手把 pip 升到最新版。接下來才是安裝項(xiàng)目需要的依賴并第一時(shí)間把依賴列表固化到requirements.txt里。等到項(xiàng)目收斂需要部署到新機(jī)器時(shí)一條pip install -r requirements.txt就能復(fù)現(xiàn)完整環(huán)境而不是靠記憶“當(dāng)時(shí)好像裝過那幾個(gè)包”。遇到報(bào)錯(cuò)時(shí)我從來不追著錯(cuò)誤信息死磕而是先把當(dāng)前的sys.path和sys.executable打印出來。確認(rèn)環(huán)境再看報(bào)錯(cuò)往往問題已經(jīng)解決一半。如果我想在群里或者社區(qū)問別人我會一次性把python --version、python -m pip --version、完整的報(bào)錯(cuò)日志這三樣信息貼出來。很多人只甩一句“我裝不上”沒有版本信息、沒有日志神仙也難一眼定因。說實(shí)話像ModuleNotFoundError: No module named yagmail這種報(bào)錯(cuò)幾乎每個(gè)用過 Python 的人都會遇到。它看著簡單把你卡住的時(shí)間可能比任何高級特性都長。學(xué)會把自己從“只會寫代碼的人”變成“會管理環(huán)境的人”你往后的 Python 路上會順很多。