
如果你和我一样在CentOS上敲python -V发现还是 2.7内心多少有点崩溃。更别提装完一个新项目pip 里全是老旧包想用 pytest 跑用例结果命令行直接告诉你pytest: command not found。这篇文章就是记录我完整走一遍的实操过程从零编译安装 Python 3.8.5配置 pip 国内镜像源掌握临时指定源的方法再把 pytest 装好并解决命令找不到的坑。整个过程适用于 CentOS 7/8 等主流版本也覆盖了升级后“命令不在 PATH 中”的常见场景。不管你是刚接触 Linux 的开发新手还是被环境折腾过的老手应该都能在里边找到对应的解决思路。有些朋友可能觉得装个 Python 有什么难的下载安装包一路下一步不就行了真到了 CentOS 上你会发现图形界面不是标配服务器环境往往只有一个终端。而且系统自带的 Python 版本太老很多现代语法和第三方库根本跑不了。更要命的是如果你乱改系统自带的 Python可能会把系统工具一块儿带崩。所以自己编译安装一个独立版本是 Linux 服务器上最靠谱的做法。1. 为什么明知系统自带Python还要折腾编译一套3.8.51.1 系统自带Python的尴尬现实CentOS 为了保证系统工具稳定自带的 Python 版本通常很旧。CentOS 7 自带 Python 2.7.5CentOS 8 自带 Python 3.6 和 2.7。用旧版本跑老脚本没问题但想用现代语法、新的第三方库支持就处处受限。比如 Python 3.7 之后才有的 dataclasses、3.8 的 walrus 操作符在 2.7 环境里完全用不了。哪怕是想用个f-string格式化字符串老版本也直接给你个语法错误。更麻烦的是系统管理工具依赖 Python。比如 CentOS 7 上的 yum 就依赖 Python 2.7如果你图省事直接把系统的python软链接改掉yum 可能会挂掉。我就见过有人把 /usr/bin/python 指向新版本随后执行 yum 就开始报一堆Missing attribute错误最后只能重新恢复原来的软链接。这就是为什么很多教程都强调“不要动系统自带 python”而是单独编译安装一个独立版本通过软链接用另外的命令字区分。1.2 版本不是唯一理由依赖隔离与后续维护除了版本新编译安装的最大好处是可控。你可以自己指定安装目录比如/usr/local/python3.8完全不污染系统路径。后续想卸载直接删目录并移除软链接就行不需要像 Windows 卸载软件那样担惊受怕。另外Python 的第三方库依赖于编译环境。很多扩展包需要python3-dev或者对应的头文件。如果你装的是新版本它的头文件路径是独立的不会和系统 Python 混淆。这样即使未来系统升级也不影响你的项目环境。实际上在你安装以后一段时间内你会发现这个独立目录下的 Python 就像一个自成一派的“小宇宙”你在这个宇宙里怎么折腾都不会弄坏系统原本的运行环境。我以 3.8.5 为例是因为它的安装步骤和常见的 3.9、3.10、3.11 系列完全一致。不同版本只是源码包名字和下载地址里的版本号不同其余的 configure、make、make install 流程统统一样。你把 3.8.5 换成其他号码这篇文章的步骤照样能走通。2. 编译安装Python 3.8.5从依赖准备到软链切换2.1 开始之前先装齐这些底层依赖编译 Python 前先要把 gcc、make、zlib 等依赖装好。命令如下yum install -y gcc gcc-c make libffi-devel zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel xz-devel这些依赖缺一个就会出现莫名其妙的报错。libffi-devel对应 ctypes 模块少了它编译时可能直接报ModuleNotFoundError: No module named _ctypeszlib-devel是 pip 和虚拟环境解压时的底层支撑少了它 make install 时会有zipimport.ZipImportErroropenssl-devel影响 ssl 模块缺了会导致 pip 无法访问 https 源。我曾经遇到过忘记装 openssl-devel编译出来的 Python 能启动但一用 pip 就报 SSL 证书错误最后只能重新编译白白等了好几分钟。这里有一个小细节gcc-c很多教程里不写但某些 Python 扩展包在 pip 安装时需要 C 编译器提前装好能少踩坑。另外如果你是在内网环境优先通过 yum 本地的源安装依赖速度会更快。2.2 下载、配置、编译、安装四步走从 Python 官网下载源码包这里以 3.8.5 为例实际文件名是Python-3.8.5.tgz。下载后解压tar -zxvf Python-3.8.5.tgz cd Python-3.8.5接着配置安装路径./configure --prefix/usr/local/python3.8 --enable-optimizations--prefix指定了最终安装目录这个参数后面会用到做软链接和环境变量都依赖它。--enable-optimizations会启用一些性能优化但编译时间会明显变长。如果不追求极致性能可以去掉。我第一次编译时加了这个参数make 阶段大概跑了十几分钟去掉之后速度能快一半左右。一般开发环境无所谓生产环境建议还是保留毕竟运行时性能换来一部分。然后编译安装make -j$(nproc) make install-j$(nproc)表示用 CPU 核心数并行编译能省很多时间。编译过程中偶尔会有一两个 warning只要不报 error 就正常。有的服务器内存比较小并行编译可能内存不够这时可以不加-j或者用-j2限制一下。make install 执行完后在/usr/local/python3.8/bin下会有python3和pip3。2.3 为什么要用软链接而不是直接替换系统python如果你直接执行/usr/local/python3.8/bin/python3可以运行。但每次都要敲这么长一串很累。很多人习惯建立软链接比如ln -s /usr/local/python3.8/bin/python3 /usr/local/bin/python3.8 ln -s /usr/local/python3.8/bin/pip3 /usr/local/bin/pip3.8注意这里不要覆盖系统的python和pip命令。系统原有命令被系统工具依赖一旦替换可能引发连锁问题。所以我的习惯是带小版本号的名字比如python3.8、pip3.8。这样既方便调用又不影响系统默认命令。验证python3.8 -V pip3.8 -V看到 Python 3.8.5 就表示成功。如果你看到的是 3.8.5但pip3.8指向的 pip 版本很老也别慌先确认当前有没有其他 Python 干扰。可以通过which pip3.8看它到底在哪个目录。如果你想在 shell 里直接输入python3也能用可以在~/.bash_profile里加一行 PATH 导出。这个放到后面环境变量部分再细说因为这里一旦搞错你又要重新折腾第二遍。3. pip换源与临时指定源给包管理器插上快车道3.1 默认源慢在哪换源到底换什么pip 默认从官方 PyPI 下载包。如果你在国内网络环境官方源的速度非常不稳定大点的包可能等到让你怀疑人生。换源本质是把 pip 下载地址指向一个离你更近的镜像站。镜像站每隔几小时同步一次 PyPI 的数据绝大多数情况下包都是最新的。常见的国内镜像站有多个覆盖场景不同通常选一个稳定的大规模开源镜像站即可。因为不同源的同步时序略有差异偶尔某个源还没有最新包这时临时指定另一个源就能解决。这里我不直接给你某个特定域名因为各家镜像站的服务状态和地址都在变化今天能用的地址过几天可能就改了。我建议你直接去常用开源镜像站的首页找到 pypi 镜像说明复制那行以/simple结尾的地址。这个信息一般在你打开镜像站首页后一眼就能看到比如备注着“pypi 镜像同步周期为五分钟”。3.2 全局改配置一劳永逸pip 的全局配置文件在 Linux 下通常放在~/.pip/pip.conf或/etc/pip.conf。如果这个目录不存在手动创建就行。推荐配置如下[global] index-url https://你的镜像站地址/simple trusted-host 你的镜像站地址写完后保存立即生效。以后所有pip install都会默认走这个镜像站。trusted-host是为了跳过 https 证书校验如果你的镜像站证书正常这行也可以不加但加上能避免某些内网环境下的证书问题。我用这个方式配置后下载速度从几十 KB/s 直接跳到几 MB/s体验完全不一样。尤其是在安装numpy、pandas这类大体积包时效果立竿见影。配置时注意缩进和空格pip 的 ini 文件对格式很敏感键值对写错一行整段配置会被忽略。3.3 临时指定源与多源切换玩法有些情况下不适合改全局配置比如你只想给当前这个项目用某个源或者某个包在当前默认源上还没有最新版本。这时临时指定源非常方便pip3.8 install 包名 -i https://你的镜像站地址/simple这条命令不加任何配置只对这一次操作生效。如果镜像站 A 安装失败可以换 B 源pip3.8 install 包名 -i https://另一个镜像站地址/simple这样比改配置文件快多了。我曾经在一个大项目部署时遇到过默认源超时临时用备份源一次成功整个过程不到一分钟非常省心。除了临时指定源还可以在配置里同时设置主源和备用源。pip 会在主源出问题时自动尝试备用源但部分旧版 pip 不认这个配置。我个人的习惯是记一个主源一个备用源遇到安装失败时直接用-i切换比配置备用源更直观。4. pytest安装后的使用从跑通第一个用例到常用技巧4.1 安装pytest时注意版本与pip的对应当你已经配置好 pip 源安装 pytest 只需要一条命令pip3.8 install pytest安装完成后确认一下版本pip3.8 show pytest如果你看到类似Location: /usr/local/python3.8/lib/python3.8/site-packages的信息说明它是装在那个独立 Python 环境下的。但很多人会遇到这样的困惑我明明通过pip3.8装了为什么敲pytest没有反应这是因为 pip 安装的可执行脚本放在 Python 的 bin 目录下如果没有被加入到 PATH 中shell 当然找不到。这个问题我在后面专门用一节来排查。如果你安装的 Python 自带 ensurepip或者你用的是虚拟环境pytest 的安装位置会有所不同。但万变不离其宗装了 pytest 之后不要急着关终端先用pip show确认一下安装路径心里踏实点。4.2 一个最小用例与断言pytest 的用法非常简单。把以test_开头或结尾的文件当作测试文件里面以test_开头的函数就是测试用例。先写一个最基础的例子# test_demo.py def add(a, b): return a b def test_add(): assert add(1, 2) 3然后在终端执行pytest test_demo.py -v-v参数会输出每个用例的具体执行结果。如果一切正常你会看到 PASSED。断言失败时pytest 会把函数内的局部变量打印出来方便定位问题。这一点比一堆 print 调试舒服得多。如果你居然在执行这步时再次遇到pytest: command not found先别急着摔键盘。可以先跑python3.8 -m pytest test_demo.py -v只要这个能跑说明 pytest 已经装好了只差环境变量的问题。4.3 fixture、参数化与常用插件写复杂测试时pytest 的fixture帮你准备数据。最常见的方式import pytest pytest.fixture def sample_data(): return [1, 2, 3] def test_sum(sample_data): assert sum(sample_data) 6fixture 的作用是先执行准备函数再把返回值注入到测试用例里非常符合“准备-执行-清理”的模式。如果需要清理数据可以在 fixture 里加yieldyield之前的代码是准备之后的代码是清理pytest.fixture def temp_dir(): path mkdtemp() yield path shutil.rmtree(path)参数化也是一个高频功能一个用例同时跑多组数据pytest.mark.parametrize(a,b,expected, [(1,2,3), (0,0,0), (-1,1,0)]) def test_add(a, b, expected): assert add(a, b) expected除了内置功能pytest 还有pytest-cov统计覆盖率、pytest-html生成报告、pytest-xdist并行执行等插件。装多一点会有点依赖但项目大了很值。5. pytest命令not found完整的失联排查链路5.1 命令找不到先分清是哪一层的问题你安装了 pytest在终端输入pytest却提示command not found可能有多个层面的原因。先不要急着重装按顺序排查。常见原因有四类pytest 根本就没装或者装到了别的 Python 版本下pytest 装上了但可执行文件所在的目录不在 PATH 环境变量里你敲的命令是pytest但那个位置刚好有个系统残留脚本或者同名冲突当前用户没有对该目录的执行权限少见这就像家里东西太多钥匙明明放抽屉里但你找错了抽屉。先确认东西是否真的在再确认你找的范围是不是覆盖了抽屉。记住一条原则command not found不等于“包没装”。它只代表 shell 在它认为可能的地方没找到这个名字。5.2 逐步定位可执行文件的实际位置第一步确认当前的 Python 版本python3.8 -V which python3.8第二步确认 pytest 装到哪了pip3.8 list | grep pytest pip3.8 show pytest如果你用的是虚拟环境还要看当前环境是否激活。比如我之前的经验中明明用系统 pip 装了 pytest后来进入虚拟环境却提示找不到因为虚拟环境里没有装。就用pip list在当前环境看看。第三步找到可执行文件python3.8 -m pytest --version如果这一步能正常显示版本说明 pytest 本身没问题只是在 PATH 里找不到。如果你想确认具体路径可以运行python3.8 -c import sys; print(sys.executable)然后去这个 Python 的 bin 目录下看看。通常可执行文件名为pytest和python3.8在同一目录。比如/usr/local/python3.8/bin/pytest。5.3 环境变量与多版本Python的终极解决确认路径后解决方式就是把 bin 目录加到 PATH。在~/.bash_profile或~/.bashrc中追加export PATH/usr/local/python3.8/bin:$PATH然后让配置生效source ~/.bash_profile再次执行pytest --version大概率就好了。如果你改的是.bashrc记得下次登录时可能还要 source 一次。也有些人配置在.profile里这取决于系统登录逻辑。最稳妥的做法是把环境变量加在.bash_profile中因为每次登录都会加载它。如果你不想改 PATH也可以直接用python3.8 -m pytest这样每次都不依赖 PATH甚至多个版本共存时不会搞混。这也是为什么 pytest 官方推荐用python -m pytest而不是直接敲pytest因为-m方式总是指向当前 Python 解释器对应的模块不会找错。如果你碰到的是同一台机器有多个 Python 版本比如系统 Python、自定义编译的 Python、虚拟环境 Python建议优先使用虚拟环境。每个项目单独创建 venv项目内执行 pip install这样 pytest 命令一定可预期也不会污染全局环境。创建虚拟环境python3.8 -m venv myenv source myenv/bin/activate pip install pytest pytest --version这里面的 pip 安装一定是当前虚拟环境里的命令也会优先从当前环境的 bin 目录找到基本不会再出现 not found。这个习惯也推荐在日常项目里用起来不要图省事全局安装一堆包过段时间你会庆幸自己做了隔离。最后补充一个少见原因如果你是通过sudo pip install装的包可执行文件可能在/usr/local/bin或者/usr/bin下而当前用户的 PATH 没有包含这些目录。这种情况下要用sudo python3.8 -m pytest来排查或者检查 sudo 的安全策略。不过一般情况下以普通用户身份安装是最省心的除非你的目录权限有问题。我在这条路上踩过几次坑最大的体会是不要一看到 not found 就怀疑安装失败先跑一条python3.8 -m pytest --version它会告诉你真实答案。编译 Python 时老老实实把依赖装齐换源时找一个稳定的镜像配置到全局日常开发用虚拟环境隔离这三个习惯让我后面少折腾很多。如果你也准备在 CentOS 上安装 Python 3.8.5建议按这个顺序来依赖 - 编译 - 软链 - 换源 - 虚拟环境 - pytest。中间任何一个环节出现 not found都先回到环境变量和安装路径上找原因。等你把这一套流程跑通之后再回头看我这句话会觉得这几步真的缺一不可。