
要说这些年做Python开发让我印象最深的不是某个框架又多牛而是Python 2.7和Python 3长期各占半壁江山的那段时期。我自己就干过一件特别狼狈的事上午还在维护一个跑了七八年的Python 2.7数据分析脚本下午新需求就要求用Python 3.10起一套服务而且两个任务必须在同一台机器上同时进行。那会儿我还不懂什么叫多版本共存就是反复改PATH、反复重装解释器最后把系统命令都搞崩了。后来摸清了这里面的门道才发现Python的多版本共存没那么玄核心就三句话解释器是文件、PATH决定默认、虚拟环境隔离依赖。这篇文章把我从原理到实操、再到踩坑全过程整理一遍Windows、Linux、macOS都有方案新手照着做能少走很多弯路老手也可以对照检查一下自己环境里有没有隐藏雷。1. 为什么到今天还得面对Python 2与3共存的现实1.1 Python 2.7不是老古董而是老生产系统很多人一听到Python 2就皱眉头觉得是过时玩意。但实际去一线摸一圈就会发现2020年官方停止维护Python 2.7之后仍然有大量企业内部系统跑在2.7上。原因很简单迁移成本摆在那儿。业务代码量大、依赖的第三方C扩展没有Python 3版本、数据库驱动涉及古老的Oracle或Sybase协议、甚至连某些内部运维脚本都深度依赖旧的字符串处理逻辑——这些都不是一句赶紧升级就能解决的。我在一家数据服务公司待过公司的核心报表平台就是Python 2.7写的每天凌晨跑批算指标几千行代码稳如老狗。领导当然知道该升级但升级意味着回归测试、数据校验、调度系统重写每一环都可能把业务搞挂。所以最终决策就是老系统继续用2.7新系统一律用3.x两边并行。这不是个例银行、运营商、制造业的数据中台里这种新旧混跑的场面非常普遍。所以多版本共存不是没事找事而是现实需求逼出来的。谁掌握了优雅共存的方式谁就能在不破坏旧系统的同时放心地用新语法写新项目。1.2 共存需求从哪里冒出来的三类最常见场景第一种场景是双业务并行。像刚才说的老项目跑在2.7上新项目用3.10你上午在旧环境里修脚本下午切到新环境里写接口每天来回倒腾。第二种场景是系统工具强依赖特定版本。Linux发行版里很多系统命令依赖自带的Python比如Ubuntu 18.04上apt、software-properties都靠Python 3。你要是手贱把/usr/bin/python3删了或者替换成别的版本系统直接半瘫痪。这种不能动系统Python的约束决定了你必须在旁独立安装另一个Python版本。第三种场景是不同开发工具的版本冲突。有些自动化构建工具特别是一些老旧的C构建脚本还在用Python 2执行但你日常写的脚本、爬虫、数据分析又要用Python 3。还有npm生态里某些node-gyp编译依赖当年就是绑定Python 2的不共存就装不上。无论哪种场景本质上都指向同一个结论你需要在同一台机器上让多个不同版本的Python解释器和平共处而且还要做到互不干扰、切换自如。接下来要讲的原理就是为这件事服务的。2. 共存的前提解释器、PATH和虚拟环境的三角关系2.1 解释器本身只是一个可执行文件很多初学者会把Python想象成系统里一个神秘的东西其实它就是个可执行文件。Windows上是python.exeLinux/macOS上是python、python3、python2.7这样的二进制文件。你在终端敲python实际上是在执行某个具体路径下的程序。多版本共存的第一个基础认知就是你可以同时安装多个Python解释器它们各自躺在自己的目录里互不影响。比如Windows下C:\Python27\python.exe和C:\Python310\python.exe可以同时存在Linux下/usr/bin/python2.7和/usr/local/bin/python3.10也可以各自安好。真正的问题从来不是装不下而是你敲一个命令时系统启动的是哪一个。2.2 PATH决定你敲python时到底启动了谁PATH是一个环境变量内容是一串目录路径用冒号Linux/macOS或分号Windows分隔。当你敲下python这个命令时shell按顺序去PATH里的每个目录查找找到名字匹配的第一个可执行文件就执行。这就好比你去一条街上买东西街上开了好几家同品牌的店你只会走进第一家开门的。所以版本切换的根源问题就是哪个解释器所在目录在PATH里排得更靠前。明白了这一点很多诡异现象就能解释了为什么你明明装了Python 3.10敲python却弹出2.7因为Python 2.7的安装目录在PATH里排在前面。为什么你装了一个库脚本一跑却说找不到因为装库用的pip属于解释器A而跑脚本的python命令属于解释器B它们压根不是一家人。2.3 虚拟环境解决的真正问题沿着上面的思路往下走你会发现即使你能在多个解释器之间切换还有一个更大的坑等着你第三方库的冲突。同一个项目如果同时需要Django 1.8只能跑在Python 2和Django 3.2只支持Python 3全局环境根本装不完。虚拟环境的作用就是给每一个项目建立一个独立的site-packages目录。你激活了某个虚拟环境后python命令自动指向这个环境里的解释器pip install也默认装进这个环境的目录和全局环境、和其他项目彻底隔离。所以我的建议非常明确**版本切换用工具py启动器、pyenv、update-alternatives依赖隔离用虚拟环境venv、virtualenv。**这两个东西是配合关系不是二选一。很多人只装了不同版本的解释器却在全局环境里裸奔装依赖最后版本倒是切过去了包全乱套了。2.4 一张表理清python、python2、python3之间的关系命令默认指向典型来源说明python取决于PATH顺序发行版自带或手动安装Linux里可能是python2也可能是python3不同系统差别很大python2Python 2.x最新安装版本发行版或源码安装明确指定用二代python3Python 3.x最新安装版本发行版或源码安装明确指定用三代python3.10精确到小版本的Python 3.10官方安装包、源码编译版本号精确到minor最保险pyWindows的Python启动器官方安装包自动带通过py -3、py -2.7指定版本不依赖PATH顺序这张表看起来简单但很多人就是栽在python到底是谁这个问题上。我见过太多人在Linux上敲python发现是2.7就以为系统里没有Python 3其实python3明明就躺在同一个目录里。遇到问题第一步永远是执行which python或者where python看清楚自己正在用的到底是谁再往下一步排查。3. Windows环境下的多版本共存实操用py启动器切换一切3.1 安装阶段就要做对的三个选择Windows上装多个Python版本最推荐的方式就是下载官方安装包一个版本一个版本地装。装的时候有三个选择直接影响后面的体验第一务必勾选Add Python to PATH。这个勾选的作用是把当前这个版本的主目录加进PATH这样你至少能直接通过路径访问它。当然多个版本都勾选的话PATH里最后勾选的会排在前面但这问题不大因为我们有py启动器兜底。第二推荐选择Install for all users。这样安装路径会放在C:\Program Files\Python38这类固定目录而不是用户目录下的隐藏路径。道理不是权限问题而是路径更规范、更好找。第三全部装完后打开CMD执行py -0看看能不能列出所有已安装的版本。如果能看到类似- 2.7.18、- 3.8.10、- 3.10.12这样的输出说明py启动器已经识别到它们了。这一步能做的就别拖装完马上验证。3.2 py命令的日常用法py启动器是Windows下Python多版本共存的灵魂工具。它本身也是一个可执行文件安装Python时自动装进C:\Windows\System32或C:\Windows里所以不管PATH怎么乱敲py基本都能找到它。日常最常用的几个命令py -2.7 xxx.py用Python 2.7跑脚本py -3 xxx.py用最新安装的Python 3跑py -3.8 xxx.py精确地用3.8跑py -0列出所有已注册的Python版本py -0p列出版本的同时显示解释器的完整路径py -V查看py启动器当前默认使用的Python版本这个设计最妙的地方在于它完全绕开了PATH的顺序问题。不管你的环境变量被折腾成什么样只要py启动器在你总能精确指定想要的解释器版本。所以我在Windows上写项目脚本一律用py -3.8 main.py这样的方式启动而不是裸敲python main.py。3.3 设置PY_PYTHON让默认版本可控py启动器也支持通过环境变量控制默认版本。最核心的两个变量是PY_PYTHON和PY_PYTHON3。PY_PYTHON的值可以是3或2表示不带版本号调用py时默认用三代还是二代。PY_PYTHON3则可以精确到小版本比如设成3.8那py默认就用3.8即使你之后又装了3.10也不会改变。设置方法就是普通的Windows环境变量配置系统属性 → 环境变量 → 新建或编辑。设完之后重开CMD执行py -V验证。这里有一个很实际的场景公司老项目统一跑2.7新项目统一跑3.8那你就可以在工作电脑上把PY_PYTHON设成2.7日常敲py直接进老环境新环境通过py -3.8显式进入。如果哪天新项目变成主力再把变量改回来一行都不用动。3.4 快速验证当前环境的命令组合版本混装之后最容易出现的一种情况就是你在终端里敲python -V显示3.10敲py -V也显示3.10你以为两者是同一个东西其实不一定。python命令可能来自PATH里的某个安装目录而py命令通过注册表去找默认版本两者可以指向完全不同的解释器。所以每装完一个新版本、每切换一次环境建议都执行这三条命令对比where python py -0p python -m pip -Vwhere python告诉你裸敲python时实际执行的是哪个文件py -0p列出版本和路径python -m pip -V则能显示当前python对应的pip版本以及它的安装路径。把这三个信息对齐了你对当前环境就有底了。4. Linux/macOS环境从源码编译到pyenv的共存之路4.1 不推荐直接替换/usr/bin/python的替代方案在Linux上很多人第一反应是把/usr/bin/python软链接指向Python 3.10觉得这样一步到位。这个操作我强烈不建议尤其是在Debian/Ubuntu系统上。原因很简单系统的很多工具比如apt、gnome-terminal、software-properties-common内部脚本是依赖特定版本Python的你强行把/usr/bin/python换成别的版本轻则这些工具运行报错重则直接导致包管理器崩溃系统都没法正常更新。正确的思路和Windows一样新版本不要覆盖系统已有的把新解释器装到独立目录然后通过工具管理调用优先级。4.2 第一步用源码编译或系统仓库补全Python版本在Linux上装一个额外的Python版本最常见的两种方式一种是直接用系统包管理器。Ubuntu/Debian可以试试apt install python3.8 python3.8-venvCentOS/Rocky可以试试yum install python38。这种方式简单装完直接有/usr/bin/python3.8但版本不是很新仓库里有什么装什么。另一种是源码编译。从python.org下载对应版本的tgz包然后按这个流程走wget https://www.python.org/ftp/python/3.8.12/Python-3.8.12.tgz tar -xzf Python-3.8.12.tgz cd Python-3.8.12 ./configure --prefix/opt/python3.8 make -j$(nproc) sudo make install编译之前记得先确保系统装了编译依赖Ubuntu上可以执行sudo apt install build-essential zlib1g-dev libssl-dev libffi-dev libbz2-dev否则你后面用pip装加密、压缩相关的包时各种报错。指定--prefix/opt/python3.8是为了把整个解释器装进独立目录不碰系统原有版本的任何文件。macOS用户相对省心用Homebrew直接brew install python3.8 python2.7装完注意看输出提示一般会提醒你某些版本是keg-only的需要通过brew link --force或者配置PATH才能直接使用。4.3 配置update-alternatives管理python命令优先级Debian系Linux有一个好用的工具叫update-alternatives专门用来管理同名命令的软链接指向。安装好多个Python版本之后可以把它们统一登记进去sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 sudo update-alternatives --install /usr/bin/python python /usr/local/bin/python3.8 2 sudo update-alternatives --install /usr/bin/python python /usr/local/bin/python3.10 3命令末尾的数字是优先级数字越大越优先。登记完执行一下交互式切换sudo update-alternatives --config python它会列出所有登记过的版本让你输入数字选择默认项。切完之后执行python -V确认。这里我得说一个经验优先级只是默认值真实项目里千万别依赖默认最好在项目文档里写明用python3.8跑或者直接通过虚拟环境锁定。因为同一台机器上除了你还有别的项目、别的同事大家不可能都遵守你脑子里的潜在约定。4.4 用pyenv接管用户级版本切换的进阶玩法对于不想动系统环境、又需要频繁切换版本的场景我强烈推荐用pyenv。这个工具可以不依赖管理员权限在用户级别管理多个Python版本。安装方式在pyenv的GitHub仓库里有脚本装完后配置shell环境变量export PATH$HOME/.pyenv/bin:$PATH eval $(pyenv init -)使用起来很简单pyenv install 3.10.12 pyenv install 2.7.18 pyenv global 3.10.12 # 全局默认 pyenv local 2.7.18 # 当前目录自动切到2.7 pyenv versions # 列出所有已管理版本pyenv的实现原理其实就是在PATH前面插入一个shim目录通过软链接把python命令动态指到对应版本。你进入哪个目录这个目录下的.python-version文件写着哪个版本python就自动变成那个版本整个过程完全无感。pyenv还可以搭配pyenv-virtualenv插件实现版本切换虚拟环境二合一。我现在个人的主力电脑就是这么配置的各个项目都有自己的Python版本和环境互不干扰非常清爽。5. 版本切换后最容易被忽视的一环pip与第三方包的归属5.1 pip、pip2、pip3、python -m pip的区别很多人以为pip是一个独立的工具实则不然。每个Python版本在安装时都会带上一个对应的pipWindows下你会在不同版本的Scripts目录里看到pip.exe、pip2.exe、pip3.exe等Linux下更是有pip2、pip3、pip3.8之分。问题就出在你直接敲pip install xxx时系统从PATH里找到的pip不一定是你想要的那个版本对应的pip。举个典型例子你的PATH顺序是Python 2.7在前、Python 3.8在后那敲pip可能跑到2.7的site-packages里装东西可你的脚本是用3.8跑的自然就找不到模块。所以我在任何多版本环境下都坚持一个铁律不用裸pip一律用解释器 -m pip。python3.8 -m pip install requests py -3.8 -m pip install requestspython -m pip的意思是用明确指定的Python解释器去执行pip模块装的包一定进这个解释器的环境不存在歧义。5.2 自动激活脚本与虚拟环境的组合虚拟环境是隔离依赖的正解但很多新手用起来总是差一步。Windows下激活是myenv\Scripts\activateLinux/macOS下是source myenv/bin/activate。激活之后终端提示符前面会出现环境名这时你再执行python -m pip install包会装进虚拟环境自己的site-packages里。这里有两个大坑我踩过无数回。第一个是忘了激活就装包结果依赖全跑进全局环境第二个更隐蔽你在终端A激活了虚拟环境又开了一个终端B以为B也自动带着环境结果B还是全局Python。虚拟环境的激活状态是跟随终端会话的不会跨窗口生效。所以每次开新终端跑项目前先检查一下自己当前落在哪个环境里。一条命令搞定which python echo $VIRTUAL_ENVWindows对应的是where python和echo %VIRTUAL_ENV%如果VIRTUAL_ENV为空说明当前压根没激活。5.3 PYTHONPATH污染问题的根治有些教程会教你在全局环境变量里设置PYTHONPATH让所有Python项目都能import到某个公共目录。这在单版本时代勉强能忍在多版本共存时代绝对是灾难源头。不同版本的解释器会按自己的sys.path去加载模块一旦你设了全局PYTHONPATHA版本的模块很可能被B版本误加载轻则版本冲突重则直接崩溃。我的做法是全局层面完全不设置PYTHONPATH。如果确实需要额外路径写进虚拟环境的activate脚本里或者干脆在代码里用相对路径配合sys.path动态插入。这样无论哪个版本都不会因为一个全局变量而互相污染。5.4 模块安装方向性的检查清单多版本环境里装依赖每次装之前都按这个顺序过一遍确认你要服务的是哪个项目、项目用的解释器是哪个版本确认当前终端有没有激活虚拟环境没激活就先激活用python -m pip install -r requirements.txt安装而不是裸pip装完用python -c import xxx; print(xxx.__version__)验证这套流程看着啰嗦但能替你挡住80%的我明明装了却找不到模块问题。真实环境里很多同事来求助我让他们跑一下which python和python -m pip -V问题当场就暴露了。6. Python 2和3的兼容代码怎么写才能两头吃6.1 用__future__和兼容库缓解分歧如果老项目短期不能迁移到Python 3但你又希望代码尽量两头跑Python官方早就给你留了后门__future__导入。在Python 2的文件开头写上对应的future导入就能提前启用Python 3的语法特性from __future__ import print_function from __future__ import division from __future__ import unicode_literals后两个特别有用。division让Python 2的5/2也返回2.5而不是2unicode_literals让字符串字面量默认按unicode处理和Python 3行为一致。另外还有six库和future库。six提供了一堆兼容函数比如six.moves.range、six.iteritems让你不用管版本差异future库则更激进试图让Python 2代码看起来完全像Python 3。我的建议是老项目用six做兼容垫片就够了别过度引入新工具增加心智负担。6.2 常见语法差异速查Python 2和3的语法差异真正影响日常开发的其实就那几处做成表格一眼扫完差异点Python 2Python 3print语句print helloprint(hello)整数除法5/2等于25/2等于2.5字符串类型str是字节串unicode是文本str是文本bytes是字节串迭代器range返回列表xrange返回迭代器range返回range对象类似迭代器异常语法except Exception, e:except Exception as e:input函数raw_input()读取字符串input()读取字符串字典视图dict.keys()返回列表dict.keys()返回视图由于这些差异的存在从Python 2迁到3不能靠简单的全局替换。官方工具2to3能把print、except这类明显语法改掉但字符串语义、库变化这些它管不了。真正靠谱的迁移方式是先让代码在2.7环境下用future导入跑通再切换到3.x逐个模块验证。6.3 写一套代码同时跑2和3的五条经验我自己维护过一段时间一套代码双版本的项目总结几条血泪经验第一统一用print函数。任何地方都不要用print语句全部print()。第二涉及文件读写明确指定encoding。Python 2和3的文件默认编码处理完全不一样最省事的方式是open(filename, r, encodingutf-8)别依赖默认。第三处理网络数据时分清文本和字节。Python 2里response.read()返回字符串Python 3里返回bytes直接混用必炸。建议统一用six.ensure_text或bytes.decode()显式转换。第四迭代器优先。能用生成器就用生成器能返回iter()就不返回列表避免版本间的返回类型差异。第五依赖库的版本务必锁定。同一个库在Python 2和Python 3下的最新版本可能分叉了比如Django在2.7下只能用1.11在3.8下能用3.x两个版本的行为差异很大。写进requirements-py2.txt和requirements-py3.txt两套清单别混在一起。7. 多版本共存踩坑记录与一次完整排查过程7.1 坑一pip安装到了另一个Python这个坑我刚开始混合环境工作时几乎每周都踩。场景是这样的我在项目里要用requests执行pip install requests安装提示成功结果脚本一跑直接ModuleNotFoundError: No module named requests。排查链路是这样的先which pip看pip在哪个目录再which python看python在哪个目录两个路径一对发现根本不是一个解释器的。原因就是我前面说的裸pip和python命令在PATH里指向了不同版本。解决办法倒简单统一用python -m pip install requests把解释器和装包动作绑定到同一个人。这个坑本身不是配置问题而是使用习惯问题但能坑到大量人所以就值得单独拎出来说。7.2 坑二脚本shebang写死导致系统定时任务爆炸Linux下写Python脚本第一行如果是#!/usr/bin/python就把解释器路径写死了。问题在于系统升级或版本切换后这个路径下的Python可能被替换了你的脚本莫名其妙跑在了错误的版本上。最典型的就是cron定时任务里执行脚本cron不加载你shell里那些环境变量它按shebang指定的路径找解释器找谁算谁。我遇到过一次特别诡异的情况cron定时任务每天凌晨3点跑一个Python 2的报表脚本某天突然全部乱码。排查半天发现是运维同学把/usr/bin/python的update-alternatives切换到了Python 3而脚本里全是Python 2语法直接全挂。解决方式脚本开头写成#!/usr/bin/env python2.7通过PATH到具体版本或者干脆在cron命令行里写死绝对路径/usr/local/bin/python2.7 /opt/report/generate_report.py。宁可多打几个字也不要把命运交给系统默认值。7.3 坑三Windows报python was not found的真相热搜词里反复出现python was not found; run without arguments to install from the Microsoft Store这个错误我帮人处理过很多次。罪魁祸首不是你没装Python而是Windows应用商店的应用执行别名机制。新版Windows 10/11里C:\Users\用户名\AppData\Local\Microsoft\WindowsApps目录下有几个0字节的python.exe和python3.exe占位文件它们的作用是把命令重定向到微软商店的下载页面。一旦你的PATH里这个目录排在Python安装目录前面终端敲python就会触发这个占位符而不是启动你装好的解释器。解决办法是在设置 → 应用 → 高级应用设置 → 应用执行别名里关闭python.exe和python3.exe两个开关。或者把Python安装目录从PATH里移到WindowsApps目录之前。处理完再敲python -V世界就清净了。7.4 一个实际的排查过程有一次同事火急火燎找我说我在虚拟环境里装了numpy脚本一跑还是报No module named numpy。我远程帮他走了一遍完整的排查链路第一步which python看他在跑哪个解释器。结果显示的是/usr/bin/python不是虚拟环境里的解释器心里就有数了。第二步看有没有激活虚拟环境执行echo $VIRTUAL_ENV输出是空的说明当前终端根本没激活。第三步让他看一下自己是在哪个终端装的numpy、哪个终端跑的脚本。果不其然装包的时候用的是另一个终端当时激活了虚拟环境后来他新开了一个终端直接跑脚本环境全丢了。第四步让他source venv/bin/activate重新激活再跑脚本问题消失。整个排查过程没有高深技术核心就是一条先确认解释器是谁再确认环境是谁最后才往代码层面找问题。很多环境类报错按这个顺序走一分钟就能定位千万别一上来就debug代码。8. 从个人电脑到团队协作多版本环境的工程化管理8.1 requirements.txt与lock文件多版本共存如果只是一个人自己的电脑怎么折腾都行。但一旦进了团队就必须把环境管理规则化。最基本的动作就是每个项目必须记录依赖及对应的Python版本。简单项目用一个README里写明Python 3.8以上就够稍微正式一点就建两套文件requirements-py2.txt和requirements-py3.txt分别Lock住两代语言下的依赖版本。生成命令是python2.7 -m pip freeze requirements-py2.txt python3.8 -m pip freeze requirements-py3.txt这份文件提交到Git仓库后任何人在新机器上都能通过一行命令还原环境。别嫌这个步骤啰嗦团队里最能浪费效率的事情就是环境对不上。8.2 引入tox进行多版本自动化测试如果你的项目确实要兼容Python 2和3靠人工手动切换版本测试不现实。用tox做一个矩阵化的自动测试环境是最省心的方案。项目根目录放一个tox.ini内容类似[tox] envlist py27, py38, py310 [testenv] deps pytest -rrequirements-py2.txt commands pytest tests/tox会为每个环境创建独立的虚拟环境自动安装对应依赖然后逐个跑测试。这样每次提交代码本地跑一遍tox就能知道改动在哪些Python版本上通过、哪些版本上挂了不用自己手动来回切换解释器。CI里也可以配置对应的job把tox集成进去做到多版本自动验证。8.3 Docker作为最后的手段如果某个老项目实在顽固编译依赖复杂到在主力电脑上装环境都费劲那就别在自己机器上死磕了。直接用Docker把整个运行环境打包成镜像是最彻底的隔离方案。比如老项目跑Python 2.7一行Dockerfile就搞定FROM python:2.7-slim WORKDIR /app COPY requirements-py2.txt . RUN pip install -r requirements-py2.txt COPY . . CMD [python, main.py]这样开发环境里只需要一个Docker其他什么都不用装。新项目也可以基于python:3.10-slim打一个镜像两个镜像互不干涉比在宿主机上折腾各种版本切换干净得多。当然Docker也不是万能的。Linux容器不能直接跑在Windows宿主机上会有WSL相关的性能损耗构建镜像和拉取基础镜像也需要一些时间老项目的C扩展在容器里编译可能还要额外装编译工具链。但对于能不能干净地共存这个目标来说Docker是终极答案。我个人在实际项目中多版本共存的排序是这样的日常个人开发用pyenv加虚拟环境轻量又灵活Windows上依赖py启动器团队项目用requirements文件配合tox做版本矩阵验证遇到极老极重的系统直接Docker镜像解决。这套组合拳打下来版本问题就不再是问题了。最后再分享一个小习惯不管哪种方案都养成在项目根目录写清楚Python版本和依赖来源的习惯哪怕只是两行说明半年后再回来看你会感激自己当初多写了那两行。