
上个月清理笔记本磁盘的时候我发现一个一直想处理但始终没动手的问题Anaconda目录整整占了16G而且每次conda create一个干净环境都要等上快半分钟依赖解析更是动不动就卡住一两分钟。正好那段时间看到很多人在聊 uv说它是用 Rust 写的下一代 Python 包管理工具速度极快能同时替代 conda、pip、pip-tools、virtualenv 这一堆工具。我心想反正之前的习惯也到了该重构的时候干脆把主力环境从 Anaconda 切到 uv 试试。这一试就是三个月期间踩了不少坑也改掉了几个长期依赖 conda 养成的坏习惯。如果你也在纠结“要不要从 Anaconda 换到 uv”或者已经装了 uv 但不知道从哪下手这篇文章应该能帮到你。我会先讲清楚为什么值得换再把完整的迁移步骤、命令对照和坑点都列出来最后聊聊换完之后工作流发生了哪些变化。1. 换之前先想清楚conda 真正让你难受的点是什么1.1 环境管理到底在解决什么问题Python 的包管理乱不是一天两天了。不同项目依赖同一个库的不同版本A 项目要 requests 2.xB 项目要 requests 3.x直接装进全局环境很快就会互相打架。所以虚拟环境这个概念核心就一件事隔离把每个项目的依赖装进各自独立的目录。conda 在这个领域的历史地位不用我多说它以前最强的点是能分发二进制包不光是 Python 库连底层的 C 库、CUDA 驱动这种非 Python 组件都能一起管理。所以在数据科学、机器学习这个圈子里Anaconda 几乎成了默认标配。我在学校里第一次装 Python 环境就是 Anaconda那时候确实省心一个安装包搞定 Python、Jupyter、numpy、pandas不用自己配一堆东西。但“省心”是有时效的。随着项目越来越多、环境越建越多conda 的重量级问题开始盖过它的便利性。我自己的笔记本上攒了十几个 conda 环境每个环境都几百 MB 起加上pkgs目录里的缓存16G 就是这么攒出来的。平时用的时候不觉得真到磁盘吃紧、换新机器、或者要复现一个三个月前的项目时痛点就全冒出来了。1.2 conda 用久了我积累的四个真实痛点我复盘了一下自己过去两年的使用记录最频繁困扰我的不是某一个报错而是下面四个反复出现的问题痛点具体表现对我日常工作的影响依赖解析速度慢conda install要回溯历史版本、求解依赖冲突经常卡在 Solving environment装一个包等一分多钟打断思路CI 里跑一次完整环境准备要耗掉大半任务时间磁盘占用大每个环境独立复制一份包base 环境和缓存目录巨大16G 只是 Anaconda 目录还没算上各个项目.conda缓存双包管理器混用有些包 conda 没有还得pip install最终环境状态说不清楚换机器时按 environment.yml 复现经常差几个包跑起来才报错版本锁定能力弱environment.yml 可以锁版本但默认写的是范围不是精确哈希团队协作时“我这能跑你那不行”的问题反复出现没人说得清依赖到底锁没锁死这两个问题叠加起来就形成了一个很尴尬的状态conda 看起来提供了全流程管理但实际上它的环境描述文件既不能精确锁定传递依赖也不能保证换一台机器 100% 复现。尤其是当你依赖了一堆用 pip 装进去的包之后conda env export导出来的 yml 文件其实已经失真了。我意识到我真正需要的不是“更快的 conda”而是一套能把环境定义、依赖锁定、虚拟环境创建、命令执行全链路管起来而且每一步都足够快的工具。uv 就是在这个诉求下进入我视野的。2. uv 改掉了什么从解算器到缓存再到项目文件2.1 uv 为什么快Rust 实现加上全局硬链接缓存先说结论uv 快主要靠两点。第一点是它用 Rust 重写了依赖解析逻辑。conda 的 solver 要处理的东西很庞大它得考虑 Python 版本、操作系统、非 Python 二进制包的依赖关系搜索空间非常大uv 目前专注在 PyPI 生态的 wheel 和源码包上而且用并行下载、增量解析的方式把“解析依赖”这个最耗时的环节压缩到了毫秒到秒级。用我自己的实测数据uv add pandas在全新环境里大概 2-3 秒完成解析和安装同样的操作 conda 至少要 30 秒起步差距非常明显。第二点是 uv 的全局缓存机制。它会在你用户目录下建一个统一的缓存区~/Library/Caches/uv或~/.cache/uv所有虚拟环境共享同一份下载过的包安装新环境时通过硬链接把文件链接进.venv而不是重新复制一份。这就是为什么我建了十几个 uv 项目磁盘占用依然比之前一个 conda 环境小得多。提示uv 缓存的硬链接机制要求缓存和虚拟环境在同一个文件系统上这个在本地机器上一般都满足。如果你把项目放到外接硬盘或网络盘上缓存命中率会下降速度也会打折。2.2 pyproject.toml 取代 requirements.txt依赖关系更明确uv 把项目的依赖描述统一收敛到了pyproject.toml。以前 conda 时代我的依赖分散在environment.yml、requirements.txt、手动装的包三处时间一长根本记不清哪个是真依赖、哪个只是当时调试用的临时包。pyproject.toml 的好处是它把“直接依赖”和“锁定版本”分开了[project.dependencies]里写的是直接依赖可以给出版本范围比如requests2.31uv.lock文件负责精确锁定它会记录每个直接依赖和传递依赖的具体版本、哈希值、来源。日常改动时我只管往 pyproject.toml 里加依赖具体的版本解析交给 uv。每次需要精确复现环境时用uv sync按 uv.lock 安装。这种“宽入口 严出口”的设计比 requirements.txt 这种单文件既当声明又当锁定的做法清晰很多。2.3 uv 自己管 Python 版本这是 conda 时代我完全没想到的。uv 内置了 Python 版本管理能力uv python install 3.12可以直接下载一个独立的 CPython 解释器然后在项目根目录通过.python-version文件指定这个项目用哪个 Python 版本。uv venv --python 3.12创建虚拟环境时会自动找到或下载对应版本的解释器。这意味着我可以彻底告别 conda 里那套“创建环境时指定 python3.8之后想升级只能重建环境”的流程。换 Python 版本在 uv 里就是改一行.python-version然后重新uv sync的事。对我来说这是迁移之后体验提升最大的一点之一后面实战部分我会专门演示。3. 一步步把项目从 Anaconda 迁到 uv3.1 安装 uv 和过渡期共存策略安装 uv 非常简单macOS/Linux 一行命令curl -LsSf https://astral.sh/uv/install.sh | shWindows 可以用 PowerShell 脚本或者直接winget install astral-sh.uv。装完之后uv --version确认一下即可。如果你之前用 pip也可以pip install uv把它当普通工具先装上之后再考虑要不要把 pip 也换掉。我建议迁移初期不要删掉 Anaconda先让两者共存一段时间。原因很实际你手上也许有一些老脚本、旧项目它们的环境还是 conda 管的在那些项目彻底迁移完之前删掉 conda 等于自断后路。共存没有任何冲突uv 创建的环境都在各自项目目录下的.venv里和 conda 的环境目录互不干扰。我自己的过渡期大约维持了三周等所有活跃项目都切到 uv 之后才卸载 Anaconda。3.2 我的迁移流程从 environment.yml 到 pyproject.toml我挑了一个中型的项目来做迁移实验它大概有三十几个依赖里面既有 numpy、pandas 这种常见的科学计算库也有几个需要编译的包。整个迁移大概花了四十分钟核心流程如下。第一步先把 conda 环境的真实依赖列表导出来摸清家底conda env export -n myenv | grep -E ^- [A-Za-z] | sed s/^- // | cut -d -f1 deps.txt这一步不是必须的但它能让你看到这个环境里到底装了哪些直接依赖。注意conda env export导出的文件里有很多 conda 特有的包比如 openssl、libgcc 这种底层库这些不用管uv 会把它们作为传递依赖自动处理。第二步在项目根目录初始化 pyproject.tomluv init如果项目已经存在uv init会补一个最小化的 pyproject.toml不会覆盖你已有的文件。第三步逐个添加依赖把依赖分成运行依赖和开发依赖两类uv add numpy pandas scikit-learn uv add --dev pytest ruff ipykernel运行依赖写进[project.dependencies]开发依赖写进[dependency-groups.dev]语义非常清楚。这一步是最体现 uv 价值的地方uv add会立刻解析依赖树并生成 uv.lock不用等到最后再统一安装。第四步验证项目能不能正常跑uv run python main.pyuv run会自动检查.venv是否存在、依赖是否和锁文件一致不一致会先同步再执行命令。所以这一步实际包含了“创建环境 安装依赖 运行脚本”三步。迁移过程里最需要注意的细节是处理版本约束。conda 环境里很多包并不是最新版本如果你在 pyproject.toml 里直接写numpyuv 会默认装最新版这可能导致项目里某个老接口直接崩掉。稳妥的做法是在uv add时就把版本上限写清楚uv add numpy1.24,2.0 uv add pandas2.0.33.3 日常命令对照表如果你之前的主力工具是 conda下面的对照表可以帮你快速上手。我不建议把这张表当成速查手册收藏了事而是建议你实际跑一遍建立肌肉记忆。功能condauv创建虚拟环境conda create -n myenv python3.12uv venv .venv --python 3.12激活环境conda activate myenv不需要显式激活用uv run即可安装包conda install requestsuv add requests安装开发依赖conda install pytest -c conda-forgeuv add --dev pytest删除依赖conda remove requestsuv remove requests查看环境列表conda env list每个项目一个.venv不集中列出导出环境conda env export env.ymluv lock生成 uv.lock复现环境conda env create -f env.ymluv sync切换 Python 版本重建环境修改.python-version后uv sync要特别强调一下“激活”这件事。uv 的设计哲学是你不应该手动激活环境而应该通过uv run来进入这个环境。第一次用会觉得不习惯总觉得不 activate 就不放心。但用久了你会发现不激活其实是好事——你永远不会忘记自己当前在哪个环境里因为根本不存在“当前环境”这个全局状态。每个uv run都是临时进入项目环境执行一条命令执行完回到系统环境。4. 换过来之后我踩过的坑和调整过的习惯4.1 别把旧环境的包一股脑拷过来我最早想的迁移方案比较粗暴把 conda 环境里所有包列出来然后uv pip install照着装一遍。结果失败了一是有几个包在 conda 里存在但 PyPI 上根本没有二是照单全装把一堆不该有的深层依赖也带进来了环境从第一天起就是脏的。后来我调整了策略只迁移“代码里 import 过的包”到 pyproject.toml其余的一律不装。这个思路虽然麻烦一点但是值得的因为你迁移的是一个项目的真实依赖不是把你硬盘上所有环境合并成一个。如果你实在没精力一个一个筛可以用pipreqs之类的工具扫描项目代码里的 import 语句生成依赖列表然后uv pip install -r 生成的requirements.txt作为一次性过渡。但这只是过渡方案还是尽早把依赖收敛到 pyproject.toml 里比较好。4.2 编译型依赖的第一次劝退迁移过程中我第一次体验到 uv 的“快”的代价它默认优先用 PyPI 的 wheel 预编译包装得很爽但遇到某些只发布源码包、没有发布 wheel 的库时uv 会在本地现场编译这时候如果机器上缺编译工具链就会报出一堆 C/C 编译错误。我遇到的那个包需要依赖libffi和 OpenSSL 的 dev 头文件在 conda 时代这些都由 conda 的二进制包间接提供了根本不用管。换到 uv 之后需要自己保证系统里的编译工具链是完整的。macOS 上要装好 Xcode Command Line ToolsUbuntu 上要apt install build-essential python3-dev之类的。遇见这种情况不用慌解决方案也很直接要么给项目指定一个 PyPI 上已有对应 wheel 的 Python 版本要么用--no-build-isolation配合系统里已有的编译环境去编译。我的建议是如果某个库在 PyPI 上长期没有预编译的 wheel同时你的项目又重度依赖它那这个项目可以先留在 conda 里没必要为了迁移而迁移。4.3 IDE 解释器路径和缓存绕过迁移完项目之后我发现开发工具也得跟着调整。Visual Studio Code 之类的编辑器默认会找系统 Python 或 conda 环境不会自动识别项目里的.venv所以得手动设置解释器路径为项目根目录下的.venv/bin/pythonWindows 是.venv\Scripts\python.exe。还有个比较容易忽略的小点uv 可以配置镜像源来加速下载。如果是在公司内网或者国内网络环境下PyPI 官方源可能不稳定这时可以在环境变量里指定镜像export UV_DEFAULT_INDEXhttps://mirrors.xxx.com/pypi/simple如果你不想全局改也可以在项目目录建一个.env文件或者在uv add时加--index-url参数。这个配置本身不难难的是意识到“uv 的下载配置和 conda 是两套体系”。我在迁移初期经常下意识找.condarc的配置后来才反应过来这套配置文件早就跟 conda 一起退役了。4.4 版本锁文件带来的“不自由”刚开始用 uv.lock 那几天我总觉得别扭以前 conda 里想升级某个包conda update 包名直接完事uv 里每次 upgrade 都要过一遍锁文件感觉被约束了。后来我在一次线上事故里才想明白这种“约束”其实是保护。那次事故是因为我手动升级了一个传递依赖结果带动了另一个包的大版本变化项目在测试环境躺了两小时。从那以后我彻底接受了锁文件的玩法。uv 里升级依赖也不复杂分两种全部升级uv lock --upgrade重新解析所有依赖并更新锁文件单独升级uv lock --upgrade-package requests只把 requests 以及受它影响的依赖一起升级。锁定和升级是分开的两个动作这在团队协作里特别管用。因为锁文件保证了所有人在同一时刻拿到的依赖树是一致的而升级则是一个显式的、有意识的决策而不是某个人在自己机器上偷偷pip install --upgrade之后产生的随机结果。5. uv 带来的几个新工作流用过就回不去了5.1 uv tool 管理命令行工具环境不再相互污染以前装命令行工具比如 ruff、httpie、poetry 等我都是直接pip install到 base 环境里这样做的坏处是 base 环境越来越臃肿而且工具之间的依赖可能会互相覆盖。uv 里有uv tool install它把每个命令行工具装进独立的工具环境之后通过uv tool run执行环境完全隔离。uv tool install ruff uv tool run ruff check .还有更轻量的uvx它用于“只临时跑一次某个包里带的 CLI”的场景uvx cowsay hellouvx会临时创建一个环境、安装 cowsay、执行命令、然后环境自动销毁。这种“用完即走”的模式对提高日常效率帮助很大我再也不需要为了跑一个小工具而污染任何环境。5.2 一条命令跑脚本uv run 脚本.pyuv 对单文件脚本的支持也很惊艳。以前我写一个处理数据的临时脚本如果它依赖 pandas我得先想清楚当前在哪环境、有没有 pandas、要不要装。现在 uv 支持 PEP 723 标准的内联依赖声明在脚本顶部注释里写清楚依赖然后直接uv run script.py它会自动创建隔离环境并运行# /// script # dependencies [pandas2.0] # /// import pandas as pd print(pd.__version__)uv run my_script.py依赖、环境、运行一条龙。我经常把一些需要临时用到特定库的分析脚本发给自己或同事对方直接就能跑不用再问“装了什么包”“用的哪个环境”。5.3 CI 和团队协作中的改变最后说说团队协作层面的变化。之前我用 conda 时新同事入职把项目跑起来至少要 20 分钟期间还经常出现“我明明按要求装了环境但还是缺包”的问题。换成 uv 之后流程变得极其统一克隆仓库uv sync跑测试uv run pytest。没了。环境创建、依赖安装、锁文件校验全部自动完成。我在 CI 里也只写两行uv sync --frozen uv run pytest--frozen参数是不允许修改锁文件确保 CI 严格按 uv.lock 安装。如果发现有依赖漂移CI 会直接报错问题在代码审查阶段就被拦截了。这在团队里带来的结果是大家聊“环境问题”的时间骤减不再有“我这能跑你那不行”的玄学。后来我们干脆把仓库里的 requirements.txt 都删掉了所有依赖统一走 pyproject.toml 和 uv.lock连文档都简洁了不少。我个人目前最满意的状态是包里不再有几十个 conda 环境每个项目一个.venv总磁盘占用比之前一个 Anaconda 目录还小。最后分享一个小技巧如果你之前像我一样经常怀疑“这个包到底装没装”可以试试uv tree它能打印当前环境的完整依赖树哪个包依赖了哪个包、版本是不是有冲突一眼就能看明白。这套切换做完之后我的环境管理省心程度直接上了一个台阶。