境管理實(shí)戰(zhàn):從依賴沖突到高效隔離)
1. 為什么一個(gè)“環(huán)境管理器”值得單獨(dú)寫一篇?jiǎng)側(cè)胄心菚?huì)兒我最怕聽到的一句話就是“你本地跑得通我這邊怎么報(bào)錯(cuò)”。同一個(gè)腳本同事的機(jī)器上跑得歡快換到我的環(huán)境就拋出一堆ImportError或者版本沖突排查半天發(fā)現(xiàn)是numpy版本差了零點(diǎn)幾個(gè)小版本。這種問題在數(shù)據(jù)科學(xué)、機(jī)器學(xué)習(xí)、科學(xué)計(jì)算這幾個(gè)圈子里尤其常見因?yàn)檫@類項(xiàng)目依賴的庫又多又雜彼此之間還經(jīng)?!按蚣堋薄naconda就是在這種背景下進(jìn)入我視野的它本質(zhì)上是一套Python和R語言的發(fā)行版本核心價(jià)值不在于它預(yù)裝了多少庫而在于它自帶了一個(gè)叫conda的包管理和環(huán)境管理工具。你可以把它理解成一個(gè)“項(xiàng)目隔離艙”每個(gè)項(xiàng)目一個(gè)獨(dú)立房間房間里裝什么版本的Python、什么版本的第三方庫全由你說了算房間之間互不干擾。這篇文章適合剛接觸Python、被環(huán)境問題折磨過的初學(xué)者也適合已經(jīng)用了一段時(shí)間但沒系統(tǒng)梳理過conda用法的開發(fā)者。我會(huì)從設(shè)計(jì)思路講到實(shí)操細(xì)節(jié)再到踩過的坑盡量把每個(gè)“為什么”都說清楚。2. 先搞懂Anaconda到底解決了什么問題2.1 依賴沖突的根源在哪里Python的包生態(tài)非常龐大但包與包之間、包與Python解釋器之間都存在版本依賴關(guān)系。舉個(gè)常見的例子項(xiàng)目A需要pandas 1.5而pandas 1.5又依賴numpy 1.20項(xiàng)目B需要pandas 2.0它依賴numpy 1.22。如果你把這兩個(gè)項(xiàng)目裝在同一個(gè)全局環(huán)境里numpy只能有一個(gè)版本要么滿足A要么滿足B總有一個(gè)項(xiàng)目會(huì)出問題。更麻煩的是有些庫還會(huì)依賴特定版本的C編譯庫或者系統(tǒng)級(jí)組件手動(dòng)去調(diào)和這些關(guān)系幾乎是不可能完成的任務(wù)。conda的設(shè)計(jì)思路就是把每個(gè)項(xiàng)目的依賴樹單獨(dú)解析、單獨(dú)安裝、單獨(dú)存放從物理上隔離開來這樣項(xiàng)目A和項(xiàng)目B各自擁有自己的numpy互不干涉。2.2 conda和pip的分工與邊界很多人會(huì)問我已經(jīng)有pip了為什么還要用conda這個(gè)問題我當(dāng)年也糾結(jié)過。簡單說pip是Python包管理器它只管Python包c(diǎn)onda是跨語言的包和環(huán)境管理器它不僅能管Python包還能管R包、C庫、甚至一些非Python的二進(jìn)制工具。更重要的是conda在安裝包的時(shí)候會(huì)做依賴求解它會(huì)通盤考慮所有包的版本約束給出一個(gè)兼容的組合而pip是逐個(gè)安裝、逐個(gè)解析遇到?jīng)_突往往要等到運(yùn)行時(shí)才暴露。在實(shí)際工作中我的習(xí)慣是優(yōu)先用conda裝那些有復(fù)雜二進(jìn)制依賴的庫比如numpy、scipy、pytorch用pip裝那些純Python的、conda倉庫里沒有的包。兩者可以混用但要注意順序一般先conda后pip避免pip把conda裝好的依賴覆蓋掉。2.3 發(fā)行版與miniconda的取舍Anaconda官方提供了兩個(gè)主要版本完整的Anaconda發(fā)行版和精簡的Miniconda。完整版預(yù)裝了幾百個(gè)科學(xué)計(jì)算相關(guān)的包安裝包體積大裝完之后占用空間也大但好處是開箱即用常用的庫基本都在。Miniconda只包含conda和Python本身其他包按需安裝體積小、靈活度高。我個(gè)人的建議是如果你剛開始學(xué)不確定會(huì)用到哪些庫或者磁盤空間不緊張直接上完整版省事如果你已經(jīng)明確知道自己需要什么或者要在多臺(tái)機(jī)器上部署Miniconda更清爽。我自己的開發(fā)機(jī)用的是Miniconda因?yàn)槲也幌矚g預(yù)裝一堆用不上的東西而且按需安裝能讓我更清楚每個(gè)項(xiàng)目的依賴構(gòu)成。3. 核心概念拆解環(huán)境、包、通道3.1 環(huán)境是什么為什么要分環(huán)境環(huán)境environment是conda的核心概念你可以把它看作一個(gè)獨(dú)立的目錄里面包含了一個(gè)特定版本的Python解釋器、一套獨(dú)立的第三方庫、以及相關(guān)的可執(zhí)行文件。當(dāng)你激活某個(gè)環(huán)境時(shí)命令行里的python、pip等命令都會(huì)指向這個(gè)環(huán)境內(nèi)部的版本而不是系統(tǒng)全局的版本。這樣做的好處是你可以在同一臺(tái)機(jī)器上同時(shí)擁有Python 3.8、3.9、3.10、3.11等多個(gè)版本每個(gè)版本下又可以有不同的庫組合。我通常會(huì)給每個(gè)項(xiàng)目建一個(gè)獨(dú)立環(huán)境命名上帶上項(xiàng)目簡稱和Python版本比如proj_analysis_py310這樣一眼就能看出這個(gè)環(huán)境是給誰用的、基于哪個(gè)Python版本。3.2 包管理的底層邏輯conda安裝包的時(shí)候并不是簡單地把文件復(fù)制到某個(gè)目錄而是會(huì)做幾件事首先從配置的通道channel里下載包的元數(shù)據(jù)然后根據(jù)當(dāng)前環(huán)境里已安裝的包和待安裝包的依賴聲明做一次依賴求解找出一組滿足所有約束的版本組合最后下載對(duì)應(yīng)的二進(jìn)制包并解壓到環(huán)境目錄中。這個(gè)過程比pip的逐個(gè)安裝要慢一些但換來的是更高的成功率。conda的包通常是預(yù)編譯好的二進(jìn)制包不需要在本地重新編譯這對(duì)那些依賴C擴(kuò)展的庫來說非常友好省去了配置編譯器的麻煩。3.3 通道的選擇與優(yōu)先級(jí)通道channel是conda下載包的來源默認(rèn)是官方通道但很多包并不在官方通道里而是在社區(qū)通道比如conda-forge里。conda-forge是一個(gè)社區(qū)維護(hù)的通道包的數(shù)量和更新速度都優(yōu)于官方通道我現(xiàn)在的習(xí)慣是把conda-forge設(shè)為默認(rèn)通道或者至少放在高優(yōu)先級(jí)。配置通道可以通過命令行參數(shù)臨時(shí)指定也可以寫進(jìn).condarc配置文件里永久生效。需要注意的是通道優(yōu)先級(jí)會(huì)影響依賴求解的結(jié)果如果不同通道里有同名但不同構(gòu)建的包c(diǎn)onda會(huì)按照優(yōu)先級(jí)來選擇。我踩過的坑是有時(shí)候從某個(gè)通道裝的包和另一個(gè)通道的包不兼容導(dǎo)致環(huán)境里出現(xiàn)奇怪的運(yùn)行時(shí)錯(cuò)誤后來統(tǒng)一用conda-forge就少了很多這類問題。4. 從零開始環(huán)境創(chuàng)建與日常操作4.1 安裝后的第一步配置裝完Anaconda或Miniconda之后我建議先做兩件事一是配置通道二是設(shè)置是否自動(dòng)激活base環(huán)境。配置通道可以通過conda config --add channels conda-forge來添加然后conda config --set channel_priority strict開啟嚴(yán)格優(yōu)先級(jí)這樣能減少依賴沖突。關(guān)于base環(huán)境新版本的conda默認(rèn)不自動(dòng)激活base我覺得這個(gè)改動(dòng)挺好的因?yàn)閎ase環(huán)境應(yīng)該保持干凈只放conda本身和一些基礎(chǔ)工具日常開發(fā)都用自己的項(xiàng)目環(huán)境。如果你習(xí)慣自動(dòng)激活可以用conda config --set auto_activate_base true改回來但我個(gè)人不建議。4.2 創(chuàng)建、激活與退出環(huán)境創(chuàng)建一個(gè)新環(huán)境的命令是conda create -n 環(huán)境名 python版本號(hào)比如conda create -n demo_py310 python3.10。這里指定Python版本很重要因?yàn)椴煌?xiàng)目對(duì)Python版本的要求可能不同。創(chuàng)建完成后用conda activate 環(huán)境名激活激活后命令行的提示符前面會(huì)出現(xiàn)環(huán)境名提醒你當(dāng)前在哪個(gè)環(huán)境里。退出用conda deactivate。我剛開始用的時(shí)候經(jīng)常忘記激活環(huán)境直接在base里裝包結(jié)果把base搞得亂七八糟后來養(yǎng)成了習(xí)慣打開終端第一件事就是conda activate到目標(biāo)環(huán)境。另外刪除環(huán)境用conda remove -n 環(huán)境名 --all這個(gè)操作不可逆刪之前確認(rèn)一下環(huán)境里有沒有還沒備份的配置。4.3 包的安裝、更新與卸載在激活的環(huán)境里安裝包用conda install 包名指定版本用conda install 包名版本號(hào)。更新包用conda update 包名更新所有包用conda update --all但后者要謹(jǐn)慎因?yàn)橐淮涡愿滤邪赡軙?huì)引入不兼容的版本組合我一般只在新建環(huán)境時(shí)用老環(huán)境里盡量按需更新。卸載包用conda remove 包名。如果某個(gè)包在conda通道里找不到可以退而求其次用pip install 包名但裝完之后最好用conda list看一下確認(rèn)pip裝的包沒有把conda裝的依賴覆蓋掉。我遇到過好幾次pip把numpy降級(jí)的情況導(dǎo)致其他依賴numpy的庫報(bào)錯(cuò)后來就盡量先查conda有沒有沒有再考慮pip。4.4 環(huán)境導(dǎo)出與復(fù)現(xiàn)團(tuán)隊(duì)協(xié)作或者換機(jī)器的時(shí)候需要把環(huán)境配置導(dǎo)出成文件方便別人復(fù)現(xiàn)。命令是conda env export environment.yml這個(gè)文件里記錄了環(huán)境名、通道、所有包及其精確版本。別人拿到這個(gè)文件后用conda env create -f environment.yml就能創(chuàng)建一個(gè)一模一樣的環(huán)境。不過這個(gè)文件里會(huì)包含一些平臺(tái)相關(guān)的包跨平臺(tái)復(fù)現(xiàn)時(shí)可能會(huì)出問題這時(shí)候可以用conda env export --from-history只導(dǎo)出你顯式安裝的包讓conda在目標(biāo)平臺(tái)上重新求解依賴。我一般會(huì)兩種都導(dǎo)出environment.yml用于同平臺(tái)精確復(fù)現(xiàn)requirements.txt用pip freeze生成作為補(bǔ)充方便那些習(xí)慣用pip的人。5. 實(shí)操中的高頻問題與排查思路5.1 依賴求解失敗的常見原因依賴求解失敗是conda使用中最常見的問題報(bào)錯(cuò)信息通常是“Solving environment: failed”后面跟著一堆沖突說明。原因主要有幾類一是通道優(yōu)先級(jí)設(shè)置不當(dāng)導(dǎo)致conda在錯(cuò)誤的通道里找包二是Python版本和某些包的版本不匹配比如你用了Python 3.11但某個(gè)包只支持到3.9三是環(huán)境里已經(jīng)裝了某個(gè)包的舊版本和新裝的包沖突。排查的時(shí)候我會(huì)先看報(bào)錯(cuò)信息里提到的沖突包然后嘗試用conda install 包名 --dry-run模擬安裝看看conda給出的解決方案是什么。如果實(shí)在解不出來可以嘗試新建一個(gè)干凈的環(huán)境只裝必要的包往往能繞過歷史遺留的沖突。5.2 環(huán)境激活失敗的排查有時(shí)候conda activate會(huì)報(bào)錯(cuò)提示“CommandNotFoundError”或者“Your shell has not been properly configured”。這通常是因?yàn)閏onda的初始化腳本沒有加到shell的配置文件里。解決辦法是運(yùn)行conda init它會(huì)自動(dòng)修改你的shell配置比如.bashrc或.zshrc然后重啟終端或者source一下配置文件。如果用的是比較冷門的shell可能需要手動(dòng)配置。我在一臺(tái)老機(jī)器上遇到過這個(gè)問題當(dāng)時(shí)用的是fish shellconda init不支持后來手動(dòng)把conda的初始化代碼加到了fish的配置文件里才解決。5.3 磁盤空間占用過大的處理conda的環(huán)境和包緩存會(huì)占用大量磁盤空間尤其是當(dāng)你創(chuàng)建了很多環(huán)境、裝了很多包之后。包緩存默認(rèn)在用戶目錄下的pkgs文件夾里可以用conda clean --all清理掉不再需要的緩存文件。環(huán)境本身如果不用了及時(shí)用conda remove -n 環(huán)境名 --all刪掉。我見過一個(gè)同事的機(jī)器上攢了二十多個(gè)環(huán)境磁盤空間被吃掉了幾十GB后來清理了一輪才騰出空間。建議每隔一段時(shí)間用conda info --envs看一下有哪些環(huán)境不用的就刪掉保持清爽。5.4 常見問題速查表問題現(xiàn)象可能原因解決思路依賴求解失敗通道優(yōu)先級(jí)沖突、版本不匹配檢查通道配置嘗試新建干凈環(huán)境激活環(huán)境報(bào)錯(cuò)shell未初始化運(yùn)行conda init并重啟終端pip裝包后conda包異常pip覆蓋了conda裝的依賴優(yōu)先用conda裝pip裝完后檢查conda list磁盤空間不足緩存和環(huán)境過多conda clean --all刪除不用的環(huán)境環(huán)境導(dǎo)出后無法復(fù)現(xiàn)平臺(tái)相關(guān)包不兼容用--from-history導(dǎo)出或補(bǔ)充requirements.txt6. 一些讓效率翻倍的小技巧6.1 用別名簡化常用命令conda的命令有些比較長比如conda activate、conda deactivate我習(xí)慣在shell配置文件里加幾個(gè)別名比如alias caconda activate、alias cdconda deactivate這樣敲起來快很多。不過要注意別和系統(tǒng)自帶的命令沖突比如cd是切換目錄的命令我就改成了cda和cdd。這種小優(yōu)化看起來不起眼但每天用幾十次累積下來能省不少時(shí)間。6.2 環(huán)境命名要有規(guī)律環(huán)境名如果隨便起過一段時(shí)間就忘了哪個(gè)環(huán)境是干什么的。我的命名規(guī)則是“項(xiàng)目簡稱_py版本”比如nlp_demo_py39、cv_test_py310這樣一眼就能看出用途和Python版本。如果同一個(gè)項(xiàng)目需要多個(gè)環(huán)境比如一個(gè)用于開發(fā)、一個(gè)用于測試可以在后面加后綴比如nlp_demo_py39_dev、nlp_demo_py39_test。這個(gè)習(xí)慣讓我在環(huán)境多起來之后依然能快速定位不用一個(gè)個(gè)激活去看里面裝了什么。6.3 定期清理與備份我每個(gè)月會(huì)花幾分鐘做一次環(huán)境維護(hù)先用conda info --envs列出所有環(huán)境把確定不用的刪掉然后用conda clean --all清理緩存最后把還在用的環(huán)境導(dǎo)出成environment.yml存到項(xiàng)目的版本控制里。這樣即使機(jī)器出問題也能快速恢復(fù)。備份的時(shí)候注意把environment.yml和項(xiàng)目代碼放在一起別只存本地最好提交到代碼倉庫里換機(jī)器的時(shí)候直接拉下來就能用。6.4 關(guān)于base環(huán)境的使用建議base環(huán)境是conda安裝后默認(rèn)存在的環(huán)境我的建議是盡量別在base里裝項(xiàng)目相關(guān)的包讓它保持干凈。base里只需要有conda本身、Python解釋器、以及一些通用的命令行工具比如git、wget。項(xiàng)目相關(guān)的包全部裝到獨(dú)立環(huán)境里。這樣做的好處是即使某個(gè)項(xiàng)目環(huán)境出了問題base依然是好的你還可以用base來創(chuàng)建新環(huán)境、修復(fù)問題。我見過有人把所有包都裝在base里結(jié)果base越來越臃腫最后依賴沖突到無法創(chuàng)建新環(huán)境只能重裝Anaconda代價(jià)很大。7. 和其他工具的配合使用7.1 與Jupyter Notebook的集成Jupyter Notebook是數(shù)據(jù)科學(xué)常用的交互式開發(fā)工具默認(rèn)情況下它只能看到base環(huán)境里的Python內(nèi)核。如果你想讓Jupyter能切換到其他conda環(huán)境需要手動(dòng)注冊(cè)內(nèi)核。步驟是先激活目標(biāo)環(huán)境然后安裝ipykernel再運(yùn)行python -m ipykernel install --user --name環(huán)境名 --display-name顯示名稱。這樣在Jupyter的kernel列表里就能看到這個(gè)環(huán)境了。我一般會(huì)給每個(gè)常用環(huán)境都注冊(cè)一下切換起來很方便。需要注意的是注冊(cè)內(nèi)核只是讓Jupyter能找到這個(gè)環(huán)境環(huán)境里的包還是要在對(duì)應(yīng)的conda環(huán)境里裝。7.2 與VS Code的配合VS Code對(duì)conda的支持很好安裝Python插件后它會(huì)自動(dòng)檢測到conda環(huán)境你可以在左下角切換解釋器。我通常會(huì)在項(xiàng)目目錄下放一個(gè).vscode/settings.json里面指定python.defaultInterpreterPath為對(duì)應(yīng)環(huán)境的Python路徑這樣打開項(xiàng)目時(shí)自動(dòng)就用對(duì)了環(huán)境。另外VS Code的終端也可以自動(dòng)激活conda環(huán)境需要在設(shè)置里開啟python.terminal.activateEnvironment。這些配置一次搞定后面就省心了。7.3 在CI/CD流水線中的使用如果項(xiàng)目要用到持續(xù)集成conda環(huán)境也可以納入流水線。常見的做法是在CI配置里用Miniconda安裝器裝一個(gè)精簡的conda然后用conda env create -f environment.yml創(chuàng)建環(huán)境再運(yùn)行測試。這樣能保證CI環(huán)境和本地環(huán)境一致減少“本地能跑、CI報(bào)錯(cuò)”的情況。我在幾個(gè)項(xiàng)目里用過這種方式效果不錯(cuò)關(guān)鍵是environment.yml要維護(hù)好每次加了新依賴就更新一下。8. 我踩過的幾個(gè)典型坑第一個(gè)坑是混用conda和pip導(dǎo)致依賴被覆蓋。有一次我在一個(gè)環(huán)境里先用conda裝了numpy后來用pip裝了一個(gè)依賴numpy舊版本的包pip二話不說把numpy降級(jí)了結(jié)果另一個(gè)依賴新版本numpy的庫直接報(bào)錯(cuò)。從那以后我裝包之前都會(huì)先conda search一下確認(rèn)conda倉庫里有再用conda裝實(shí)在沒有再考慮pip而且pip裝完之后一定用conda list檢查一遍關(guān)鍵依賴的版本。第二個(gè)坑是環(huán)境導(dǎo)出文件里包含了平臺(tái)相關(guān)的包。有一次我把在Linux上導(dǎo)出的environment.yml發(fā)給用Windows的同事他創(chuàng)建環(huán)境時(shí)一堆包找不到因?yàn)橛行┌赪indows上沒有對(duì)應(yīng)的構(gòu)建。后來改用--from-history導(dǎo)出只保留顯式安裝的包讓conda在目標(biāo)平臺(tái)上重新求解問題就解決了。這個(gè)教訓(xùn)是跨平臺(tái)復(fù)現(xiàn)時(shí)不要導(dǎo)出完整的包列表只導(dǎo)出你主動(dòng)安裝的那些。第三個(gè)坑是忘記激活環(huán)境。剛開始用的時(shí)候我經(jīng)常打開終端就直接conda install結(jié)果裝到了base里。后來我在shell配置里加了一個(gè)提示讓命令行提示符顯示當(dāng)前conda環(huán)境名這樣一眼就能看出有沒有激活。另外我養(yǎng)成了習(xí)慣打開終端第一件事就是conda activate到目標(biāo)環(huán)境然后再做其他操作。第四個(gè)坑是環(huán)境建得太多太亂。有一段時(shí)間我每個(gè)小腳本都建一個(gè)環(huán)境結(jié)果攢了三十多個(gè)自己都記不清哪個(gè)是哪個(gè)。后來我定了規(guī)則只有依賴差異大的項(xiàng)目才單獨(dú)建環(huán)境小腳本共用一個(gè)通用環(huán)境。環(huán)境名也統(tǒng)一了命名規(guī)范定期清理不用的。這樣環(huán)境數(shù)量控制在十個(gè)以內(nèi)管理起來輕松很多。9. 關(guān)于是否值得用的個(gè)人看法如果你只是寫寫簡單的Python腳本標(biāo)準(zhǔn)庫就能滿足需求那確實(shí)沒必要上Anaconda系統(tǒng)自帶的Python加上pip就夠了。但如果你涉及數(shù)據(jù)分析、機(jī)器學(xué)習(xí)、科學(xué)計(jì)算或者需要同時(shí)維護(hù)多個(gè)依賴不同的項(xiàng)目那conda帶來的隔離性和依賴求解能力是值得的。我自己的體驗(yàn)是前期花一點(diǎn)時(shí)間學(xué)習(xí)conda的用法后期能省下大量排查環(huán)境問題的時(shí)間。尤其是團(tuán)隊(duì)協(xié)作的時(shí)候一個(gè)environment.yml就能讓別人快速復(fù)現(xiàn)你的環(huán)境溝通成本降低很多。當(dāng)然conda也不是銀彈它有時(shí)候依賴求解慢、磁盤占用大這些問題可以通過合理配置和定期維護(hù)來緩解??偟膩碚f對(duì)于需要管理復(fù)雜依賴的Python項(xiàng)目Anaconda是一套成熟且實(shí)用的工具鏈值得花時(shí)間掌握。