本人刚从 java 、c#转过来,因为工作需要,这两天开始研究 python 工程。
(其实以前也接触过,但只限于会写些代码层次的基础语法)
现在新人有个疑惑,为什么 python 会有.venv 这种概念,资料说是项目编译环境隔离,每个项目有自己的解释器环境什么的。其实这种给每个项目搞内环境部署,很像是 java 开发里给每种服务的 docker 容器都单独打包一个独立的 jdk ?

Python 解释器的执行需要依赖环境(你导入的各种包),每个项目需要的环境不一样,为了避免冲突就隔离出来。而 Java 和 C#可以将环境编译到目标文件里,没有这种问题。同理 node.js 也有 node_package 。

说白了就是早起项目设计的问题,一开始没做好依赖隔离的机制(全局安装包),只能后期不断打补丁解决。。。

python 不支持同一个库有多个版本同时存在,只能用虚拟环境隔离

不能向下兼容,每个包需要的环境还不一样

因为 python 是很古老的语言,原来它只想安份地做一个 linux 下面的简单脚本语言。什么模块化包管理的工程问题不是它关心的问题。后来没法了。

如果你用 docker 容器打包 python 项目的话, 可以不用 venv 的如果你用宿主机的 Python 运行多个项目的话, 每个项目要一个 venv 避免依赖包的版本冲突

你们 java 也有 classpath 这个概念啊,稍微现代点的语言配套的依赖管理哪个没有这种版本隔离设计?你写 java 时候所有依赖都会扔进全局$CLASSPATH 吗?无非只是 Python 处理 classpath 简单粗暴一点,按 java 的理解方式,直接创建一个新的 JAVA_HOME ,然后 java 复制一份进去,然后再把然后依赖扔进去。本质都是为了去隔离依赖,只是 java 生态里( maven 、gradle 之类的)没这么简单粗暴而已。如果你想知道为什么要隔离依赖,1 防止依赖冲突 2 方便部署,就是这么简单。

Java/C#对应的就是运行时版本问题其实依赖包也算在运行时的一部分

意思是高版本的项目环境要导入某些包,但这些包一开始设计时采用的了低版本环境的,项目就会导包运行失败?

python 很多包不同版本不兼容,又无法同时安装用一个包的多个版本。如果 1 台机器运行多个 python 程序,1 个程序用了高版本的包,另外 1 个程序用了低版本的包,那么这 2 个程序就无法同时使用,为了解决类似问题,需要使用 venv

也没有类似 maven 隔离的工具吗?

python 是否有类似 maven 这类自动构建依赖隔离的工具?

#12 venv 就是你要的隔离工具呀,只不过具体实现方案和 maven 差别有些大就是了

python 不需要编译构建,python 因此没有构建工具,依赖隔离的工具可以看 miniconda

venv 实际上就可以理解成 node 的 node_modules

可以了解一下 poetry python-poetry.org/

你搞 java 难道没用过 pom.xml ?一个意思。只不过 Python 没出息,抄的 node_modules 那一套。虽然不咋地,比没有强点。

用 venv 有个好处, 不用额外装 pipvenv 会给你一个 pip

别问 问就是历史原因,type hints 还导致循环依赖呢,python3 设计的时候就没考虑过这些,它跟 javascript 一样 设计就是一门简单的语言,奈何后面胶水 胶的太好了,javascript 甚至更绝,动态类型 1 "1" 都不用区别了,python 好歹还是强类型的动态语言, javascript "666"/2 都能正常运行

还有,用啥就学啥吧,用那个技术领域成熟的约定跟方案吧,别自己想那么多,我自己搞过 go rust javascrip typescript java grovy jetbrains ide 插件开发,哪个具体的领域都有自己构建方案 跟构建风格,以及语言哲学,说白了 这些都是社区各自的喜好,maven 也不是绝对的,当你仲裁依赖冲突的时候就知道了,明明编译通过的代码,结果运行的时候,自动仲裁可能用了老版本,新的代码用了 未实现的方法,贼鸡儿恶心

导入路径和语言解释器所在的路径有关所以只好把解释器拷贝很多份

我还是更不理解为啥有的前端喜欢用 pnpm ,反过来学习古老的 python

因为 python 默认是所有项目共享第三方依赖的。pip install 会把库安装到操作系统里影响所有项目。也就是 A 项目如果需求 depend:v1.1 ,B 项目就用不了 depend:v1.0

请按照 python 的习惯写 python而不是写 python 语法的 java ,没必要就别过度封装,不要弄一堆服务,接口。

你的回答是不是离题了。。

JS 有运行时类型,任何一个变量的类型都可以通过 typeof 查看,只是 JS 在+,==这类运算符发现类型不匹配会自动尝试转换类型,这个是 ECMA script 规定的,现在看来属于是历史遗留问题了

就是一开始没设计好包隔离的问题,或者说当时没有这样的设计。导入路径(site-packages)完全由 python 解释器路径来计算,可以说是一个相当 naive 的方案。后来有了这种需求,于是有人就想到可以虚拟化解释器,建立软链接,伪装成一个独立的 Python ,发现问题解决了!但其实这种方法相当 hacky 。大家习惯了反而没有什么动力去推动包导入机制的革新了,于是就这样用下来了。

参考文章: frostming.com/2019/03-13/where-do-your-packages-go/

java 不是也有 maven 嘛

venv 给的不就是额外装的吗...

你看看 venv 里面都配置了啥就明白了。 本质上 venv 就相当于一个傻瓜化的 本地 manve 仓库。 venv 实现的原理就是配置 PYTHONHOME 等 Python 运行时需要的环境变量来实现的。 这就和 java 常见的运行时 java -jar -classpath=xxx 是一个意思。 只不过 venv 傻瓜化操作, 一键就把这些配置都写成 bash 脚本,然后把包都移动到 venv 目录里的 lib 下面了而已。 如果你愿意的话, 这些操作你完全可以按照 java 的思路去手动配置。

python 没有 venv 概念。别被他们骗了你把所有依赖复制到项目 lib 目录下,代码里 os.path.insert 一下就搞定了哪里那么多废话。如果别人要引用你的项目,把你的代码全部打成一个 zip 包丢给对方就行了 docs.python.org/3/library/zipimport.html 跟 jar 包甩来甩去一样方便!别被他们骗了!!!别被他们骗了!!!别被他们骗了!!!别被他们骗了!!!别被他们骗了!!!

上面应该是 sys.path.insert 写错了~

但其实这不妨碍它设计这些东西的时候,是考量了给非专业人士用的原因,但是今天 javascript 实际上是给开发者用的,就像很多设计,像 win32 的拖拽窗口也是考量给 UI 非编程人士用的,但实际上都是开发人员在用

没有离题,因为我没写过 java ,就没有你的“为什么 python 会有 venv”的疑问,也就是说,不是 python 这种设计本身有问题,而是你习惯了其他语言的模式后,把其他语言的习惯带到 python 这边才会有此疑问。

3 楼正解,就是 pip 不支持同一个库的不同版本同时存在,才搞出了 venv, pipx 的一大堆;rvm 有 gemset 的概念,但是我们 Ruby 的项目也不用,因为 rubygem 可以允许在全局存在同一个库的不同版本

本质上全都是环境隔离的问题. 实现上看在哪一个做, 能做到什么维度.不过全是对信任不友好..由于 go 的代码要写的繁琐, 而去用 Python, 结果折腾了半天...我还是重新用 go 写 mvp 了... 体力活交给了 AI ..

最近刚好学 python ,涨见识了

以前 IOT 上部署 python 就是这么搞的

python 的包管理已经没救了。一路涌过来,发现没有任何一个包管理工具是靠谱,稳定的。conda 的依赖解析甚至是一个 NP-Hard 搜索问题,以后只会越来越慢。我现在都是 conda-lock 锁定所有版本,在不同操作系统上锁定一份,单最近发现 conda-lock 生成初始化锁定文件动辄都是几个小时。这个 python 的版本控制从在 python 推翻 2 的时候,就应该重置,现在这样,只会越来越难用。

然后发现不同平台有不同的本地库依赖:比如常见的那些机器学习项目,直接复制的话几乎肯定会炸( win->linux)

.so 文件?那是你打包技术不行。关 py 什么事。。就说了,如果弄 JNI 不一样的问题。

#32 但凡看到别人用 sys.path.insert 我就想给他一脚

看到我 32 楼贴那个链接没?点开他,拉到最下面看看是啥?课后习题:找到该代码作者 blame 出来,上 gayhub 踢他。

因为 Python 没有 node_modules ,也没有 Maven

啊?!

“为什么要有虚拟环境”这个问题比较好回答,就是为了隔离。我觉得楼主真正想问的是,或者说这个问题有意义的点在于,为什么 Python 没有“现代”一些的包管理机制?楼上已经有不少人提到了,Python 不支持同时安装一个包的多个版本。要解释“为什么”不支持,那就比较麻烦了。这要回到 Python 最早设计包这个概念的时候,当初的决定导致了,不可能在不做 breaking change 的情况下,改变这个行为或者说添加版本机制。这个设计决策的时间点在 1.X 版本,估计大部分人都没有用过,好在 PEP 文档还是可以一窥设计思路的。我这里提个看上去无关的问题,为什么 Python 需要 all 来支持 from XXX import *?答案是 Python 的 import 机制是基于文件系统的,包名就是路径名。基于文件系统就意味着,包名无法区分 XXX/Xxx/xxx 的,因为文件系统不确定是否是大小写敏感的。你可能会觉得包名就是路径名,那给包名后面加上版本号不就行了吗?理论上是的,很多现代一点的语言的包管理就是这么做的。那 Python 为什么做不到?主要原因是 Python 流行起来之后,不可能在不影响生态的情况下做这样的改动了。理论上 Python 2/3 的时候有这个机会,但是 Python 没有做这个变动,这与 import 的实现机制有关。Python 的 import hook 包括标准库,都涉及到自举( bootstrap ),所以是用 C 写的。更重要的是,增加版本号意味着 Python 要官方实现包管理机制,而包管理是个理论和实践都非常难的事情。Python 选择继续采取社区的“虚拟环境”方案,我个人认为这是个正确的选择。这个问题到这里其实就差不多说清楚了,不过我打算再补充一些内容。包管理问题的难点在于版本选择,目的是要找到某个包的完整的(所有)、兼容的(不能包含相冲突的,比如同一个包的两个版本)依赖。这个问题有多难?我印象大概 2017 年才有证明,这是一个 NP 完全问题。NP 完全的意思就是,不知道是否存在一个可以稳定在多项式时间内,完成这个解析过程的算法。所以几乎所有的传统包管理软件只能做个二选一,要么选择正确但是有可能很慢,要么选择时间可控但是可能出错。实际上几乎所有的包管理都选择正确但是慢,或者说正确但是不清楚要花多长时间。前面的证明给了包管理器新的设计思路,要增加限制条件或者说妥协,不去追求完美解决 NP 问题,只做工程上“好用”的解决方案。具体理论不展开了,我这里简单说一下“现代”包管理的两个基础假设。一是高版本总是向后兼容低版本,不兼容的情况使用 semantic versioning 的 MAJOR 区分。二是主包可以改写间接依赖的版本。第二条不好理解,举个例子:A 是开发者要构建的包,这里叫主包,它直接依赖 B 和 C==1.2.0 ,其中 B 又依赖 C==1.1.0 。这时候要构建 A ,那么 A 对于 C 的要求就会覆盖 B 对于 C 的要求,又因为 C 的 MAJOR 版本没有变,理论上 C==1.2.0 是同时满足 A/B 需求的。当然单独构建 B 的时候,B 作为主包,依旧会使用 C==1.1.0 的版本。这一条放宽了之后,NP 问题的限制就不存在了,这个问题就有了多项式级别的解法。我接触过的包管理,似乎只有 Rust(cargo)/Go 是基于新理论(即主动规避 NP 问题)的。区别是 Rust 总是选最高版本,而 Go 选最低版本。Go 的实现比 Rust 简介很多很多。其他的包管理都还在跟 NP 问题作斗争。

#47 再做一点补充。Python 3 是 2008 年左右发布的。这个时间点上,如果 Python 决定重写 import hook ,将版本号纳入成为包名的一部分,支持安装同一个包的多个版本,就没有今天虚拟环境什么事了。这个改动和“不做官方包管理”不冲突。不做官方包管理是正确的选择,在那个时间点上,很难做得好,特别是这些实现都要用 C 来写。但是底层限制所限,同一个解释器环境不能安装同一个包的多个版本,那包管理器是永远无法摆脱虚拟环境的。

多项式时间非确定性问题吗,从数学视角研究得这么深……真的 6 的

python 的虚拟环境和其他语言的依赖管理工具是类似的,解决都是依赖管理和隔离的问题。pip 安装包时,把包安装在了系统/用户路径。而不同项目使用同一个系统/用户路径就会有依赖冲突的问题。所以有了虚拟环境这个概念,来隔离不同项目使用的不同版本的依赖。而其他语言的工具链可以直接把包安装到当前项目的路径,然后当前项目的库路径加入到库加载路径里(比如 CLASSPATH )。因此这些语言没有虚拟环境这个概念。实际上楼上也有人提到了,python 其实也可以学习这个做法,把所有包安装到项目的目录下,然后设置 PYTHONPATH 。

47 楼说到问题的本质了。话说回来按照现有的虚拟环境的方式做隔离也未尝不是一个好的方式,从解释器到依赖包再到文件路径统统可以隔离,完全在自己的掌控之内,这种感觉让我着迷。

还有所谓多版本共存的问题,这不是真正的问题所在。对于编译型的语言,编译(链接)时,会在生成的可执行的文件里记录对应的版本号。在运行时,则会根据可执行文件中记录的版本去动态链接对应的库。这里的多版本库的共存的机制其实不是语言本身的,而是 ELF 文件格式提供的(以 linux 为例,windows 应该也有类似的机制)。但对于脚本型语言,你的脚本里没有记录依赖的库版本(并且也不该在代码级别处理版本依赖)。因此即使你在安装的库的版本里,加了版本后缀作为区分又有什么用呢?因为你脚本本身没有记录到底使用的哪个版本。

这么搞我觉得 python 越来越臃肿了,时不时找不到依赖😅

说得很到位>A 是开发者要构建的包,这里叫主包,它直接依赖 B 和 C==1.2.0 ,其中 B 又依赖 C==1.1.0 。这时候要构建 A ,那么 A 对于 C 的要求就会覆盖 B 对于 C 的要求,又因为 C 的 MAJOR 版本没有变,理论上 C==1.2.0 是同时满足 A/B 需求的。这个 TIL 了。semantic versioning 在 Rust 上是可以强制的,甚至 linter 都能检查出 API breakage 。但对于一门像 Python 这样的 duck typing 的动态语言,有没有 breaking change 这属于一个薛定谔的问题。所以越来越多的包都倾向于使用 calendar versioning 了。这导致在 NP 问题上依然无解。

按理只需把 pip 改成默认只安装在当前目录下应该能解决大部分问题把