新电脑装完 Python,我干的第一件事不是去写代码,而是先把虚拟环境这套东西给配齐。不是我有洁癖,而是被环境折腾过的 Python 开发者心里都清楚:手上的项目一多,版本一乱,迟早要翻车。这边项目 A 需要老版本的爬虫库,那边项目 B 的框架撞车,再加上某个项目对 Python 小版本还挑三拣四,环境一旦混在一起,光是排查ImportError就能浪费你一下午。所以虚拟环境这件事,真的是 Python 开发避不开的第一课。
这篇博客把 venv、conda 和 Python 版本选择放在一起讲透,因为这三件事本身就是一环扣着一环的:选错 Python 版本,后面建什么环境都是埋雷;不会用虚拟环境,版本隔离无从谈起;分不清 venv 和 conda 的边界,就容易在错误的方向上较劲。文章会从隔离原理讲起,把两条主流路线的实操细节、踩坑点和选型策略都过一遍。刚入门没几天、还在给系统 Python 乱装包的新手可以看,被多版本 Python 和依赖冲突搞到头秃的老手,也能找到一些可以直接落地的思路。
1. 为什么需要虚拟环境:先理解隔离的本质
1.1 Python 环境混乱的典型惨案
先讲几个我亲眼见过、也亲身踩过的例子。第一个场景:你本地跑着一个个人博客项目,用requests老版本写死的逻辑,后来接手一个同事的爬虫项目,里头requests要求新版,于是你pip install --upgrade requests,博客项目立刻开始报奇奇怪怪的兼容错误。更麻烦的是,你根本想不起来是哪些包被动过。
第二个场景:某开发者在服务器上直接用系统 Python,手上没权限就用sudo pip install往全局塞包,后来系统工具升级,直接把某个关键依赖覆盖成了不兼容的版本,服务起不来,运维查了半天才发现是 Python 全局环境被玩坏了。
第三个场景发生在我自己身上:要跑一个深度学习项目,依赖里面有包只支持某个 Python 小版本,我又舍不得把系统默认版本改掉,结果每次运行前都要折腾PYTHONPATH和一堆环境变量,最后还是全乱了。
这三个案例指向同一个结论:Python 包的默认安装位置是全局的,多个项目共用一份site-packages,版本冲突是必然事件,只是什么时候爆的问题。虚拟环境的价值,就是给每个项目一个独立的小地盘,互不干扰。
1.2 虚拟环境隔离了什么:解释器路径、site-packages 与 PATH
要搞清楚它是怎么做到的,先理解 Python 在 import 一个模块时到底去哪儿找包。解释器会按照sys.path里的目录逐个搜索,通常就是标准库路径、site-packages还有环境变量PYTHONPATH里列的目录。你装一个第三方包,默认就落进当前 Python 对应的site-packages里。全局 Python 的site-packages只有一个,所有项目装的包都堆在这里,互相覆盖就是分分钟的事。
venv 做的事可以拆成三块。第一,在新目录里放一个指向目标 Python 解释器的软链接(Windows 下通常是复制一份),让“当前 Python 是谁”变得可控;第二,在新环境里创建独立的site-packages目录,让新装的包落在这里而不是全局目录;第三,提供一个activate脚本,修改 PATH 环境变量,让python和pip命令优先找到虚拟环境里的那一份。激活之后你敲which python,路径已经变成.venv/bin/python,pip装的包自然就进到.venv/lib/python3.x/site-packages里了。不激活也行,直接.venv/bin/python xxx.py效果一样,只是命令写得麻烦点。
conda 的隔离思路就不太一样。它不仅仅管 Python 的包,还会把 Python 解释器本身、依赖的 C/C++ 库、可执行文件目录、头文件全部放进同一个“前缀目录”里。一个 conda 环境就是一个相对完整的小系统,bin/、lib/、include/各司其职。之前我用 conda 建环境的时候打开目录看过,里面不但有python,还有一堆编译链接库。这种“环境即前缀”的思路,决定了它能管理的东西远超出纯 Python 包的范围。
1.3 venv 与 conda 的隔离机制差异
表面上 venv 和 conda 都在做环境隔离,但底层不是一个物种。
venv 是 Python 自带的官方工具,它不负责管 Python 本体。你用python3.10 -m venv建出来的环境,里面的解释器就是 3.10,换个python3.11再建一个,才能得到 3.11 的环境。它的隔离主要是给“同一个 Python 版本”的依赖提供独立空间。如果你的需求只是两个项目共用 Python 3.11 但依赖不同版本,venv 非常合适,轻量、快、干净。
conda 把 Python 本身也看成一个“包”。你可以在创建环境时直接指定python=3.10,conda 会从包仓库里把对应版本的 Python 解释器连同依赖一起装进环境。如果项目需要 CUDA、MKL 这类非 Python 的底层库,conda 也能管,因为它有自己的包格式和依赖求解器,会把二进制层面的依赖一并解析好。付出的代价是环境体积大、命令操作更重、依赖求解经常要等。
我用一句话总结两者的边界:venv 解决的是“同一 Python 版本下的依赖打架”,conda 解决的是“不同 Python 版本和不同底层库之间的组合博弈”。前者像给每个房间换了把锁,后者像直接把整层楼按户型分开装修。
2. Python 版本选择:从源头避免兼容性噩梦
2.1 版本号与 ABI:为什么不能随手拿最新版
很多新手有个误区:Python 出新版本,我直接装上用最新的,岂不是美滋滋?等到某个包在安装时报“找不到匹配版本”或编译报错时,才意识到问题不简单。
这里有个关键概念叫 ABI(应用二进制接口)。CPython 每次大版本更新,扩展模块的二进制接口都可能变化。你自己import的纯 Python 包没问题,但那些用 C/C++ 写的扩展包(尤其是打包成 wheel 的)在安装时会校验平台的 ABI 标志。你可以留意一下 wheel 包的文件名,很多长这样:numpy-1.26.0-cp311-cp311-win_amd64.whl,里面有cp311,说明这个包预编译产物只对 Python 3.11 生效。你把 Python 升到 3.12,没有对应的 wheel 就得现场拉源码编译,如果系统还缺编译工具链,直接一堆红色报错。
更麻烦的是,很多大生态库对新版本 Python 的支持是有滞后期的。深度学习的框架、科学计算类的重量级库,往往要等几个月甚至大半年才跟进新的 Python 版本。盲目追新导致的结果就是:你的代码本身没啥问题,但环境凑不齐,天天在装依赖上耗时间。
2.2 两条多版本管理路线:pyenv 与 conda
既然不同项目需要不同的 Python 版本,就得有人管住“系统里到底装了哪些 Python”。这条路主要有两个主流做法。
第一种是 pyenv。它能在用户目录下编译或下载多个 Python 版本,然后用pyenv local、pyenv global之类命令切换。装好之后,你随时可以pyenv install 3.10.13装一个指定小版本,再通过python3.10 -m venv .venv建立项目环境。pyenv 的优势是贴近原生 Python 的体验,但 Windows 上支持比较弱,通常建议在 Linux/macOS 或 WSL 里用。
第二种就是 conda。创建环境时直接指定 Python 版本,比如conda create -n py311 python=3.11,conda 会去仓库拉一个编译好的 CPython 解释器放进环境。好处是不同版本之间切换非常自然,不需要额外工具;坏处是它连 Python 解释器都是“包”的思路,整个体系比裸 Python 重。我个人的习惯是:如果是纯 Python 项目,用 pyenv 管版本,再用 venv 建环境;如果项目涉及科学计算或底层库,直接 conda,省心。
2.3 选版本的实操策略
版本这块我踩过不少坑,现在遵循一套自己的策略,按优先级排序:
先去查依赖库的支持矩阵。项目里如果引用了比较重的第三方库,就直接去官方文档看它支持的 Python 版本范围。比如某个深度学习框架在某个时期只支持到 Python 3.10,那新环境就别选 3.11,选了就是自找麻烦。
新项目不要盲目选最新发布版。一般选当前生态已经稳定适配一年以上的版本,比如现在 3.10、3.11、3.12 这类已经过了磨合期的版本都是稳妥选择,除非你的依赖明确要求更高版本。老项目则完全以项目配置文件、CI 配置和依赖锁定的requires-python为准,别凭感觉升级。
原则是:能用 venv 解决的,别拉 conda 进场;需要 conda 管底层库的,就好好用 conda 从头管到尾。这样比“用 conda 建环境再用 pip 硬装一切”要稳定太多。
3. venv 全流程实操:从建环境到发布依赖
3.1 创建、激活与退出:命令背后的机制
venv 的基本操作其实五分钟就能学会,但很多入门资料只教你敲命令,不说背后的门道。我多看了一层,所以踩坑少一些。
创建环境:
# 进入项目目录后执行 python -m venv .venv这里有个小细节值得说:命令里的python是你当前默认的 Python 版本。如果系统里同时装了 3.10 和 3.11,你想建 3.10 的环境,最好用python3.10 -m venv .venv。别想着先建了再升级,venv 没有自动换解释器的能力(--upgrade只能在同一大版本里打补丁),建错版本,删掉重来最干净。
激活环境:
# Unix / macOS / Linux 下 source .venv/bin/activate # Windows 的 cmd 或 PowerShell 下 .venv\Scripts\activate.bat .venv\Scripts\Activate.ps1激活后命令提示符前面会出现(.venv),此时which python指向环境内的解释器。pip install进来的包,全部落在.venv/lib/python3.x/site-packages。退出环境用的是deactivate,这是个 shell 函数,不是可执行文件,所以别去which找路径,找不到很正常。
创建好环境后,你进目录看一眼结构,会看到bin/(Windows 是Scripts/)、lib/python3.x/site-packages/,还有根目录下的pyvenv.cfg。这个配置文件记录了 home(指向宿主的 Python 安装路径)、version 和是否使用系统包。如果你想用.venv/bin/python直接跑脚本而不激活,也是完全可以的,很多 CI 脚本就是这么干的,更少受 shell 环境干扰。
3.2 依赖锁定与复现:requirements.txt 的坑与更稳的做法
装完依赖,下一步就是让环境可复现。最常见的做法是:
pip freeze > requirements.txt但这项操作有几个坑我都要说清楚。pip freeze会把环境里所有包全部列出来,包括间接依赖,这没问题,问题在于它会把一些本地开发模式的包也输出成-e git+...或-e /path/to/pkg的形式,这种行在别处复现环境时会失败。另外,它导出的版本号是当前环境锁死的结果,跨平台时未必完全可靠。
我更推荐用 pip-tools 这套流程。先用requirements.in记录顶层依赖(你自己主动装的那些包),然后执行:
pip-compile requirements.in -o requirements.txt pip install -r requirements.txtpip-compile会解析依赖树并锁定所有间接版本,还会自带合理的约束注释。相比手工维护requirements.txt,这样生成的锁定文件更干净、更可审查。每次想升级某个包,改requirements.in里的版本范围,重新跑一遍编译即可。
如果你在团队里维护项目,我建议把锁定文件纳入版本控制,并在 README 里写明“用哪版 Python + 如何建环境”。否则换个同事、换台电脑,环境又要重新猜一遍,失去虚拟环境的意义了。
3.3 到底要不要用 --system-site-packages
venv 建环境默认隔离所有 site-packages,但有一个可选参数叫--system-site-packages,意思是在你新环境的sys.path里额外包含系统 Python 的包目录。听起来很方便:系统里已经装好的大包,环境里直接复用,省得重复下载。但代价是你的环境不再“纯净”,系统里任何包的变动都会影响这个项目,隔离的初衷就破功了。
我个人的建议是:除非你强烈需要一个系统级安装的大型包(比如某些公司内网源里的私有 wheel,放在系统 site-packages 里方便多环境共用),否则别用这个参数。正常情况下,每个环境各装各的,磁盘多占一点,但换来的是确定性和安全感。
4. conda 实操与依赖解析:科学计算场景的底层逻辑
4.1 用 conda 创建环境和指定 Python 版本
conda 的环境管理命令比 venv 多几层,但核心逻辑依然清晰。我平时用得最多的是下面这段:
# 创建环境,指定 Python 版本和常用 channel conda create -n pyproject python=3.10 -c conda-forge # 激活 conda activate pyproject # 安装依赖 conda install numpy pandas # 退出 conda deactivate注意到没有?创建环境时直接把 Python 版本写在了命令行里。你想建 3.9 就python=3.9,想建 3.12 就python=3.12。conda 会根据你的指定去仓库拉对应解释器,同时替你做依赖求解,把解释器自身依赖的底层库一并处理好。这种体验和 venv 完全不同,venv 是先有解释器再谈隔离,conda 是环境定义了解释器。
因为 conda 的依赖求解开销不小,命令执行经常得等十几秒甚至更久。我一般优先装好 mamba(conda install mamba -n base),之后都用mamba create、mamba install代替 conda 对应命令,求解速度快得多,命令语法几乎一致。科学计算项目里依赖动辄几十上百个包,这个提升是实打实的。
4.2 conda 与 pip 共存的风险:为什么混用容易炸
conda 和 pip 在同一环境里混用,属于高风险操作,搞懂原因你才知道怎么避开。
conda 装包时,使用自己的依赖求解器,会检查环境里已经装过的 conda 包,并维护一套元数据。pip 装包时只跟 Python 的 site-packages 打交道,完全不知道 conda 的存在。两边各写各的,环境里就可能出现两个同名包的不同副本(一个在 conda 管理的目录,一个在 site-packages 的 pip 目录),import 时先命中哪一个,取决于sys.path的顺序,结果就是玄学报错。
我之前处理过这样的案例:某项目在 conda 环境里已经装好某个数值计算库,后面有人用 pip 补装另一个包,结果 pip 先把相关依赖升级了一遍,把一个底层库换成了不兼容的版本,整个环境的 “conda 视角” 和 “pip 视角” 彻底分叉,最后只能重建环境。
正确姿势是:能用 conda 解决的依赖优先 conda;conda 仓库里没有(或版本太老)再考虑 pip。一旦这个环境已经大量用 pip 装过包,就尽量别再回头用 conda install 添加新依赖,至少别动已有依赖的底层库。
4.3 环境导出与复现:environment.yml 的正确打开方式
conda 导环境最常见的是conda env export,但这个命令导出的文件包含每个包的具体构建号、内部版本,跨平台复现差。你要在别人电脑上重建,大概率对不上。
我更推荐两种组合:
# 只导出你显式指定的包,干净很多 conda env export --from-history > environment.yml这个--from-history非常实用,它只列出创建环境和后续conda install时你真正写下的包名,不包含所有被动装进来的依赖。文件小而清晰,适合当项目级别的“顶层依赖声明”。在此基础上,还需要精确复现锁定时,用 conda-lock 生成带哈希的完整锁定文件,或者把 pip 依赖单独用requirements.txt管理,两边配合。
环境文件里还可以标注 channel 和 pip 依赖:
name: pyproject channels: - conda-forge dependencies: - python=3.10 - numpy - pandas - pip - pip: - requests - -r requirements.txt关键点是,用这个文件创建环境后,所有包都从 conda 和 pip 两个来源各装各的,团队的初始状态相对可复现,后续维护也方便。
5. venv 还是 conda:选型对比与混合使用策略
5.1 六个维度看透差异
很多人纠结“到底用哪个”,其实纠结的原因是没把问题拆开。我把评估维度整理成一张表,你看完基本就能自己判断。
| 维度 | venv + pip | conda |
|---|---|---|
| 核心定位 | Python 标准虚拟环境工具 | 跨语言环境与包管理器 |
| Python 版本管理 | 靠宿主机已有解释器,不自动下载 | 环境内可直接指定任意版本 |
| 非 Python 依赖 | 基本不管 | 能安装 C/C++ 库、GPU 驱动层组件等 |
| 依赖解析能力 | pip 只解析 Python 包 | conda 自建求解器,兼顾二进制层 |
| 环境体积 | 小、轻量 | 大、更重 |
| 团队协作友好度 | requirements 文件直观 | environment.yml 稍复杂但完整 |
从这个表能看出,它不是“谁替代谁”的关系,而是谁更适合什么场景的问题。venv 轻、简单、够用;conda 重、全、能力强。如果你做的是普通的 Web 项目、脚本工具、爬虫、后端服务,venv 完全够了,不必为了“统一工具”把 conda 绑进来,反而拖慢上手速度。
5.2 按项目类型选路线的直接建议
我的具体建议可以归纳成三条路线:
第一,纯 Python 项目、Web 项目、自动化脚本,用系统 Python(或 pyenv 装好的某版本)配合 venv,依赖用 pip + pip-tools 管理。团队里所有人都用相同流程,成本最低。
第二,数据科学、机器学习、需要 GPU 加速、依赖大量本地二进制库的项目,直接 miniconda + mamba。别想用 venv 硬撑,C 库层面的问题是 venv 管不动的。这时候 conda 的跨语言依赖管理真香。
第三,需要在多个 Python 版本之间同时做兼容测试的库项目,用 pyenv 装多个版本,每个版本下分别建 venv 去跑测试。如果你喜欢 conda 的手感,也可以建多个 conda 环境,各有各的 Python 版本,逻辑一样,就是更占磁盘。
5.3 我实际环境里的组织方式
我在自己的开发机上长期保持一套固定的组合:系统 Python 只作为基础,不下手装任何第三方包;pyenv 负责安装和维护多个 Python 小版本;日常项目一律.venv起步;涉及科学计算再切到 miniconda。
有个小提醒:如果你已经在 conda 环境里开发某个项目,就没有必要再在里面建一个 venv 来叠加隔离。我踩过的坑是,某同事在 conda 环境里为了跑一段临时脚本又建了个 venv,结果两套依赖体系嵌套在一起,pip 装的东西到底归谁管得澄清半天。一个项目定一种隔离方案,从一而终。
另外,不同团队协作时,所有环境相关命令最好写进 README 或者 Makefile,统一一条路径。环境管理最怕的不是复杂,而是每个成员都有自己的一套,合到一起就是灾难现场。
6. 常见问题与排查技巧实录
6.1 激活失败与 shell 配置问题
最常见的激活失败是:执行source activate pyproject报错或者进了环境但python还是老路径。这多半是因为 conda 的初始化没有写入当前 shell 配置文件。解决方式很简单,先执行conda init bash(或对应你的 shell,比如 zsh 用conda init zsh),然后重新开一个终端。venv 的激活脚本不会遇到这个问题,因为它本来就是直接 source 脚本。
Windows 上的 PowerShell 用户经常会碰到“无法加载文件 Activate.ps1,因为在此系统上禁止运行脚本”。这是 PowerShell 执行策略的限制,管理员身份执行Set-ExecutionPolicy RemoteSigned可以解掉,或者干脆不用激活脚本,直接用.venv\Scripts\python.exe调用环境内的解释器,反而更省事。
排查环境到底有没有生效,不要光看提示符前的括号,直接执行which python(Windows 是where python),看路径是否落在环境目录内。有时候 shell 的缓存会作怪,别名路径老转发到旧解释器,用hash -r清理一下再看。
6.2 幽灵依赖与 PYTHONPATH 污染
有种诡异场景:你明明激活了虚拟环境,pip list里也没看到某个包,但import xxx居然成功了。十有八九是环境变量PYTHONPATH在捣鬼。它里面列的目录优先级很高,会把虚拟环境外的包混进来。排查方法很简单:echo $PYTHONPATH,如果非空就得警惕。我个人习惯是默认不设置这个变量,需要导入自定义代码时,用包结构配合pip install -e .解决,而不是靠PYTHONPATH硬加路径。
还有一个更隐蔽的坑:忘了激活环境,然后直接pip install。此时包被装进全局 Python,项目跑起来报错你还没头绪。以后养成习惯,先确认pip --version的路径属于当前项目环境,再执行安装。也可以在项目里直接用.venv/bin/pip install。
如果你怀疑某个包到底装在哪里,pip show 包名会显示 Location 字段,指向它实际落地的 site-packages,一眼就能看出是不是当前环境。
6.3 依赖冲突与编译失败的处理思路
pip 装包时报“ERROR: Cannot install packages because of conflicting dependencies”很常见,别一股脑--force-reinstall硬上,那样只会把环境搞得更乱。可以先用pip check诊断哪些包之间依赖不一致,把冲突的包找出来,再决定降级某方或升级某方。依赖关系复杂的场景,用 pip-tools 生成锁定文件,比手工调版本稳定得多。
编译类报错,比如error: command 'gcc' failed: No such file or directory,大概率是你要装的包没有当前 Python 版本的预编译 wheel。看报错里的绿色提示,通常会说“Failed building wheel for xxx”,说明它在现场编译。解决思路有三条优先级:先找有没有对应版本的离线 wheel;没有就换低一档 Python 版本重试;最后才考虑补编译工具链硬编。多数情况下,换 Python 版本比装一堆编译依赖快得多。
如果 conda install 求解时间长得离谱,先别取消,先想想是不是没有用 mamba。换成 mamba 之后大部分项目的安装等待时间能压到 1 分钟内,亲测有效。
6.4 磁盘占用与重建环境的实用技巧
conda 环境一个就能占几个 GB,装多了磁盘告急是常事。conda clean --all能清掉缓存和没用的包,pip cache purge清 pip 缓存。我通常每月跑一次,能清理出不少空间。环境如果彻底坏了,也不要执着于修复,直接conda env remove -n 环境名删掉重建,只要锁定文件还在,重建比排查坏环境靠谱得多。venv 同理,.venv目录删掉重新python -m venv .venv,加上 requirements 文件,几分钟又是一条好汉。
还有个小技巧,conda 环境用conda clone复制一份来做升级测试,比直接升级原来的安全。我在给生产项目升级 numpy 的时候就是这么干的:克隆环境、在里面跑测试、确认没问题再动原环境。
我个人在实际操作中的体会是:虚拟环境这件事,和写代码的水平没有直接关系,它更多是“工程习惯”。你可以在一年内不碰到依赖冲突,但只要碰到一次,那一次就足以让你记住所有教训。我现在无论项目多小,哪怕是临时写个小脚本,也会先建一个虚拟环境再动手。不是仪式感,是为了让“在我电脑上能跑”这个结论至少可复现、可继承、可排查。环境管理花的时间,其实是在给未来的自己省时间。