V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
bocchi1amos
V2EX  ›  Python

为什么 Python 会有.venv 虚拟环境的概念?

  •  2
     
  •   bocchi1amos · 346 天前 · 7327 次点击
    这是一个创建于 346 天前的主题,其中的信息可能已经有所发展或是发生改变。


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

    68 条回复    2024-03-21 10:46:31 +08:00
    Juszoe
        1
    Juszoe  
       346 天前
    Python 解释器的执行需要依赖环境(你导入的各种包),每个项目需要的环境不一样,为了避免冲突就隔离出来。而 Java 和 C#可以将环境编译到目标文件里,没有这种问题。同理 node.js 也有 node_package 。
    codehz
        2
    codehz  
       346 天前   ❤️ 5
    说白了就是早起项目设计的问题,一开始没做好依赖隔离的机制(全局安装包),只能后期不断打补丁解决。。。
    Jat001
        3
    Jat001  
       346 天前   ❤️ 1
    python 不支持同一个库有多个版本同时存在,只能用虚拟环境隔离
    cslive
        4
    cslive  
       346 天前
    不能向下兼容,每个包需要的环境还不一样
    visper
        5
    visper  
       346 天前   ❤️ 2
    因为 python 是很古老的语言,原来它只想安份地做一个 linux 下面的简单脚本语言。什么模块化包管理的工程问题不是它关心的问题。后来没法了。
    cndenis
        6
    cndenis  
       346 天前
    如果你用 docker 容器打包 python 项目的话, 可以不用 venv 的
    如果你用宿主机的 Python 运行多个项目的话, 每个项目要一个 venv 避免依赖包的版本冲突
    Masoud2023
        7
    Masoud2023  
       346 天前   ❤️ 1
    你们 java 也有 classpath 这个概念啊,稍微现代点的语言配套的依赖管理哪个没有这种版本隔离设计?

    你写 java 时候所有依赖都会扔进全局$CLASSPATH 吗?

    无非只是 Python 处理 classpath 简单粗暴一点,按 java 的理解方式,直接创建一个新的 JAVA_HOME ,然后 java 复制一份进去,然后再把然后依赖扔进去。

    本质都是为了去隔离依赖,只是 java 生态里( maven 、gradle 之类的)没这么简单粗暴而已。

    如果你想知道为什么要隔离依赖,1 防止依赖冲突 2 方便部署,就是这么简单。
    crackidz
        8
    crackidz  
       346 天前
    Java/C#对应的就是运行时版本问题

    其实依赖包也算在运行时的一部分
    bocchi1amos
        9
    bocchi1amos  
    OP
       346 天前
    @cslive 意思是高版本的项目环境要导入某些包,但这些包一开始设计时采用的了低版本环境的,项目就会导包运行失败?
    yinmin
        10
    yinmin  
       346 天前 via iPhone
    python 很多包不同版本不兼容,又无法同时安装用一个包的多个版本。如果 1 台机器运行多个 python 程序,1 个程序用了高版本的包,另外 1 个程序用了低版本的包,那么这 2 个程序就无法同时使用,为了解决类似问题,需要使用 venv
    bocchi1amos
        11
    bocchi1amos  
    OP
       346 天前
    @cslive 也没有类似 maven 隔离的工具吗?
    bocchi1amos
        12
    bocchi1amos  
    OP
       346 天前
    @yinmin python 是否有类似 maven 这类自动构建依赖隔离的工具?
    lonewolfakela
        13
    lonewolfakela  
       346 天前
    @bocchi1amos #12 venv 就是你要的隔离工具呀,只不过具体实现方案和 maven 差别有些大就是了
    aapeli
        14
    aapeli  
       346 天前
    python 不需要编译构建,python 因此没有构建工具,依赖隔离的工具可以看 miniconda
    Maerd
        15
    Maerd  
       346 天前   ❤️ 1
    venv 实际上就可以理解成 node 的 node_modules
    xiaowowo
        16
    xiaowowo  
       346 天前
    可以了解一下 poetry https://python-poetry.org/
    kneo
        17
    kneo  
       346 天前 via Android
    你搞 java 难道没用过 pom.xml ?一个意思。只不过 Python 没出息,抄的 node_modules 那一套。虽然不咋地,比没有强点。
    sjtulyj
        18
    sjtulyj  
       346 天前   ❤️ 1
    用 venv 有个好处, 不用额外装 pip
    venv 会给你一个 pip
    morgan1freeman
        19
    morgan1freeman  
       346 天前
    别问 问就是历史原因,type hints 还导致循环依赖呢,python3 设计的时候就没考虑过这些,它跟 javascript 一样 设计就是一门简单的语言,奈何后面胶水 胶的太好了,javascript 甚至更绝,动态类型 1 "1" 都不用区别了,python 好歹还是强类型的动态语言, javascript "666"/2 都能正常运行
    morgan1freeman
        20
    morgan1freeman  
       346 天前
    还有,用啥就学啥吧,用那个技术领域成熟的约定跟方案吧,别自己想那么多,我自己搞过 go rust javascrip typescript java grovy jetbrains ide 插件开发,哪个具体的领域都有自己构建方案 跟构建风格,以及语言哲学,说白了 这些都是社区各自的喜好,maven 也不是绝对的,当你仲裁依赖冲突的时候就知道了,明明编译通过的代码,结果运行的时候,自动仲裁可能用了老版本,新的代码用了 未实现的方法,贼鸡儿恶心
    julyclyde
        21
    julyclyde  
       346 天前
    导入路径和语言解释器所在的路径有关
    所以只好把解释器拷贝很多份
    KgM4gLtF0shViDH3
        22
    KgM4gLtF0shViDH3  
       346 天前
    我还是更不理解为啥有的前端喜欢用 pnpm ,反过来学习古老的 python
    cheng6563
        23
    cheng6563  
       346 天前
    因为 python 默认是所有项目共享第三方依赖的。pip install 会把库安装到操作系统里影响所有项目。
    也就是 A 项目如果需求 depend:v1.1 ,B 项目就用不了 depend:v1.0
    cdwyd
        24
    cdwyd  
       346 天前 via Android   ❤️ 2
    请按照 python 的习惯写 python
    而不是写 python 语法的 java ,没必要就别过度封装,不要弄一堆服务,接口。
    bocchi1amos
        25
    bocchi1amos  
    OP
       346 天前
    @cdwyd 你的回答是不是离题了。。
    vituralfuture
        26
    vituralfuture  
       346 天前 via Android
    @morgan1freeman JS 有运行时类型,任何一个变量的类型都可以通过 typeof 查看,只是 JS 在+,==这类运算符发现类型不匹配会自动尝试转换类型,这个是 ECMA script 规定的,现在看来属于是历史遗留问题了
    frostming
        27
    frostming  
       346 天前   ❤️ 1
    就是一开始没设计好包隔离的问题,或者说当时没有这样的设计。导入路径(site-packages)完全由 python 解释器路径来计算,可以说是一个相当 naive 的方案。

    后来有了这种需求,于是有人就想到可以虚拟化解释器,建立软链接,伪装成一个独立的 Python ,发现问题解决了!但其实这种方法相当 hacky 。大家习惯了反而没有什么动力去推动包导入机制的革新了,于是就这样用下来了。
    frostming
        28
    frostming  
       346 天前
    pengtdyd
        29
    pengtdyd  
       346 天前
    java 不是也有 maven 嘛
    Davic1
        30
    Davic1  
       346 天前
    @sjtulyj venv 给的不就是额外装的吗...
    error451
        31
    error451  
       346 天前
    你看看 venv 里面都配置了啥就明白了。 本质上 venv 就相当于一个傻瓜化的 本地 manve 仓库。 venv 实现的原理就是配置 PYTHONHOME 等 Python 运行时需要的环境变量来实现的。 这就和 java 常见的运行时 java -jar -classpath=xxx 是一个意思。 只不过 venv 傻瓜化操作, 一键就把这些配置都写成 bash 脚本,然后把包都移动到 venv 目录里的 lib 下面了而已。
    如果你愿意的话, 这些操作你完全可以按照 java 的思路去手动配置。
    est
        32
    est  
       346 天前   ❤️ 3
    python 没有 venv 概念。别被他们骗了

    你把所有依赖复制到项目 lib 目录下,代码里 os.path.insert 一下就搞定了

    哪里那么多废话。

    如果别人要引用你的项目,把你的代码全部打成一个 zip 包丢给对方就行了 https://docs.python.org/3/library/zipimport.html

    跟 jar 包甩来甩去一样方便!

    别被他们骗了!!!
    别被他们骗了!!!
    别被他们骗了!!!
    别被他们骗了!!!
    别被他们骗了!!!
    est
        33
    est  
       346 天前
    上面应该是 sys.path.insert 写错了~
    morgan1freeman
        34
    morgan1freeman  
       346 天前
    @vituralfuture 但其实这不妨碍它设计这些东西的时候,是考量了给非专业人士用的原因,但是今天 javascript 实际上是给开发者用的,就像很多设计,像 win32 的拖拽窗口也是考量给 UI 非编程人士用的,但实际上都是开发人员在用
    cdwyd
        35
    cdwyd  
       346 天前 via Android   ❤️ 2
    @vituralfuture 没有离题,因为我没写过 java ,就没有你的“为什么 python 会有 venv”的疑问,也就是说,不是 python 这种设计本身有问题,而是你习惯了其他语言的模式后,把其他语言的习惯带到 python 这边才会有此疑问。
    lululau
        36
    lululau  
       346 天前
    3 楼正解,就是 pip 不支持同一个库的不同版本同时存在,才搞出了 venv, pipx 的一大堆;

    rvm 有 gemset 的概念,但是我们 Ruby 的项目也不用,因为 rubygem 可以允许在全局存在同一个库的不同版本
    veightz
        37
    veightz  
       346 天前
    本质上全都是环境隔离的问题. 实现上看在哪一个做, 能做到什么维度.

    不过全是对信任不友好..由于 go 的代码要写的繁琐, 而去用 Python, 结果折腾了半天...我还是重新用 go 写 mvp 了... 体力活交给了 AI ..
    LFL976
        38
    LFL976  
       346 天前
    最近刚好学 python ,涨见识了
    ilucio
        39
    ilucio  
       346 天前
    @est 以前 IOT 上部署 python 就是这么搞的
    thinkershare
        40
    thinkershare  
       346 天前
    python 的包管理已经没救了。一路涌过来,发现没有任何一个包管理工具是靠谱,稳定的。conda 的依赖解析甚至是一个 NP-Hard 搜索问题,以后只会越来越慢。我现在都是 conda-lock 锁定所有版本,在不同操作系统上锁定一份,单最近发现 conda-lock 生成初始化锁定文件动辄都是几个小时。这个 python 的版本控制从在 python 推翻 2 的时候,就应该重置,现在这样,只会越来越难用。
    codehz
        41
    codehz  
       346 天前
    @est 然后发现不同平台有不同的本地库依赖:比如常见的那些机器学习项目,直接复制的话几乎肯定会炸( win->linux)
    est
        42
    est  
       346 天前
    @codehz .so 文件?那是你打包技术不行。关 py 什么事。。

    就说了,如果弄 JNI 不一样的问题。
    xingheng
        43
    xingheng  
       346 天前
    @est #32 但凡看到别人用 sys.path.insert 我就想给他一脚
    est
        44
    est  
       346 天前
    @xingheng

    看到我 32 楼贴那个链接没?点开他,拉到最下面看看是啥?

    课后习题:找到该代码作者 blame 出来,上 gayhub 踢他。
    cbdyzj
        45
    cbdyzj  
       345 天前
    因为 Python 没有 node_modules ,也没有 Maven
    fgwmlhdkkkw
        46
    fgwmlhdkkkw  
       345 天前
    @kneo 啊?!
    kuanat
        47
    kuanat  
       345 天前   ❤️ 23
    “为什么要有虚拟环境”这个问题比较好回答,就是为了隔离。我觉得楼主真正想问的是,或者说这个问题有意义的点在于,为什么 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 问题作斗争。
    kuanat
        48
    kuanat  
       345 天前   ❤️ 1
    @kuanat #47

    再做一点补充。

    Python 3 是 2008 年左右发布的。这个时间点上,如果 Python 决定重写 import hook ,将版本号纳入成为包名的一部分,支持安装同一个包的多个版本,就没有今天虚拟环境什么事了。

    这个改动和“不做官方包管理”不冲突。不做官方包管理是正确的选择,在那个时间点上,很难做得好,特别是这些实现都要用 C 来写。

    但是底层限制所限,同一个解释器环境不能安装同一个包的多个版本,那包管理器是永远无法摆脱虚拟环境的。
    bocchi1amos
        49
    bocchi1amos  
    OP
       345 天前
    @kuanat 多项式时间非确定性问题吗,从数学视角研究得这么深……真的 6 的
    cnt2ex
        50
    cnt2ex  
       345 天前
    python 的虚拟环境和其他语言的依赖管理工具是类似的,解决都是依赖管理和隔离的问题。

    pip 安装包时,把包安装在了系统/用户路径。而不同项目使用同一个系统/用户路径就会有依赖冲突的问题。所以有了虚拟环境这个概念,来隔离不同项目使用的不同版本的依赖。

    而其他语言的工具链可以直接把包安装到当前项目的路径,然后当前项目的库路径加入到库加载路径里(比如 CLASSPATH )。因此这些语言没有虚拟环境这个概念。

    实际上楼上也有人提到了,python 其实也可以学习这个做法,把所有包安装到项目的目录下,然后设置 PYTHONPATH 。
    fkdtz
        51
    fkdtz  
       345 天前
    @kuanat 47 楼说到问题的本质了。
    话说回来按照现有的虚拟环境的方式做隔离也未尝不是一个好的方式,从解释器到依赖包再到文件路径统统可以隔离,完全在自己的掌控之内,这种感觉让我着迷。
    cnt2ex
        52
    cnt2ex  
       345 天前   ❤️ 1
    还有所谓多版本共存的问题,这不是真正的问题所在。

    对于编译型的语言,编译(链接)时,会在生成的可执行的文件里记录对应的版本号。在运行时,则会根据可执行文件中记录的版本去动态链接对应的库。这里的多版本库的共存的机制其实不是语言本身的,而是 ELF 文件格式提供的(以 linux 为例,windows 应该也有类似的机制)。

    但对于脚本型语言,你的脚本里没有记录依赖的库版本(并且也不该在代码级别处理版本依赖)。
    因此即使你在安装的库的版本里,加了版本后缀作为区分又有什么用呢?因为你脚本本身没有记录到底使用的哪个版本。
    allenby
        53
    allenby  
       345 天前 via Android
    这么搞我觉得 python 越来越臃肿了,时不时找不到依赖😅
    frostming
        54
    frostming  
       345 天前
    @kuanat 说得很到位

    >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 问题上依然无解。
    iorilu
        55
    iorilu  
       345 天前 via Android
    按理只需把 pip 改成默认只安装在当前目录下应该能解决大部分问题把
    kuanat
        56
    kuanat  
       345 天前   ❤️ 1
    @frostming #54

    我又看了一遍自己写的东西,其实不太准确,我这重新描述一下。

    版本选择之所以是 NP 问题,源于好几个假设。其中有几个假设是没法改变的,能够改变的还有两个:

    - 一个包可以声明自己依赖零个或多个特定版本的包。
    - 不允许同时选择同一个包的两个不同版本。

    第一条过于严格了。如果把特定版本换成一个范围,这个问题就简单多了。第二条也可以适当放宽,和第一条一样,都是建立在 semantic versioning 主动向后兼容的假设之上。虽然不能引入同一个包的 1.X.X 和 1.Y.Y ,但是可以同时引入 1.X.X 和 2.Z.Z 。

    这两个条件放宽之后,NP 问题就不存在了。但是对 APT/RPM/Node/Python 这些起步较早的社区来说,这两个要求依然太难了。

    至于主包覆盖依赖的限制,这是用来解决菱形依赖的,不是规避 NP 问题的核心措施。还是之前的例子,只要保证 C 符合 semantic versioning 向后兼容就好了。这个措施实际体现的是解决问题的思路转变。

    具体的来源比较分散记不清了,我凭印象做个总结。第一个做 NP 规避的应该是 Rust/Cargo 。之后 Go 做了一个叫 Dep 的类 Cargo 实现,实践了一段时间,吸取经验教训,重做之后形成了正式的 Go mod 方案。Rust/Go 用相同的方法解决菱形依赖,但是基础思路是不一样的。这一点从 Rust 总是选择最高版本,而 Go 总是选择最低版本能看出来。

    这里想象一个场景:A==1.1.0 依赖 B==1.5.0 ,之后 B 发布 B==1.6.0 ,由于 B 引入了非兼容改变,导致 A==1.1.0 无法依赖 B==1.6.0 构建。这个时间节点,按照 Rust 的设计,所有依赖 A 的用户(包括依赖 A 的老用户)都会受到影响,而 Go 这边只有同时依赖 A==1.1.0 和 B==1.6.0 的新用户(老用户不受影响)才会受影响。

    之后 A/B 至少有一方要打补丁发新版。Rust 认为要么 A 在 B 没有提供补丁的情况下,主动发新版声明不兼容 B==1.6.0 ,要么 B 发新版修复对 A 的支持。实际上在开源社区这两个事情都是很难的。Go 的设计是除非用户主动升级,都会保持作者发布时的最低依赖版本,(毕竟发布那个时刻的版本依赖几乎是都可以构建的)这样就给 A/B 争取了非常多的修复时间。

    Rust 的设计者更希望为开发者提供最好的体验,希望一己之力解决所有问题。Go 的设计思路是依靠社区合作,依赖所有人主动去帮助解决对自己来说比较容易,而别人不好解决的问题。所以你能看到,Go 官方一直不遗余力地推动向后兼容,因为 Go 的整套实现逻辑都强依赖整个社区对于兼容性规范的共识。



    最后赞美 pnpm ,感谢!
    cnt2ex
        57
    cnt2ex  
       344 天前
    @kuanat
    > 为什么 Python 需要 __all__ 来支持 from XXX import *
    __all__不是拿来指定哪些是公开接口,哪些不是吗?你给出的回答似乎和为什么需要__all__没什么关系???
    kuanat
        58
    kuanat  
       344 天前 via Android
    @cnt2ex #57

    这里我说得太简略了。

    在设计这个 import 机制的 1.x 版本,要实现 import * 需要去文件系统里遍历有哪些模块。底层文件系统可能会对某个名为 xyz 的模块,返回 XYZ/Xyz/xyz 几个不同的结果。当时对于这个问题的解决方案是设计 __all__ 让维护者自己声明是哪一个。

    之后的 PEP 才明确了模块名应该( should )全小写,import 的所有符号都是小写。再之后 __all__ 才成为一种工程上的控制机制。
    cybort
        59
    cybort  
       344 天前 via Android
    其实如果项目动态链接 linux 安装的包,也会有这个问题,docker 比较全面,venv 比较轻量罢了
    cybort
        60
    cybort  
       344 天前 via Android
    另外就是高版本兼容低版本在实践上很多时候是不可行,因为总有一些东西会 depreciate 掉,而要求开发者去考虑每个版本同时存在的情况,这个要求有点高了。
    flyingghost
        61
    flyingghost  
       344 天前
    @kuanat 不懂就问,包依赖的计算确实是一个无法快的计算,那么有没有人考虑过由中央包管理服务器来做这部分计算结果的缓存?这样在固定条件下,大部分包的使用者就会受益。
    我不太会估算这个缓存的存储成本,但感觉查询成本会快于本地计算哎。
    谢谢解惑。
    kuanat
        62
    kuanat  
       344 天前 via Android
    @flyingghost 60

    我没有听说过类似的方案,如果让我评论的话,我认为两个方向都不合适,一个是结合了版本号之后命中率很

    另一个方面,一个包 A 如果同时依赖 B 和 C ,我猜测不存在一个多项式算法,可以从 B 和 C 依赖计算出 A 的依赖。(需要 backtrack 到所有依赖重新计算)导致这样的缓存中根本上不可用。
    cnt2ex
        63
    cnt2ex  
       342 天前
    @kuanat #58

    我的疑问是,__all__和你提到的文件系统大小写敏感几乎没有关系。因为解决大小写不敏感所带来的问题的方案主要是通过规范要求名称都是小写。而且不管有没有__all__,import *都可以使用,只不过没有__all__会默认导入所有非下划线开头的名字。但正是这一点,让我很疑惑为什么要扯上__all__。也许你提这个目的单纯是想说__all__的诞生的历史是在规范模块名之后提出的?除此之外我看不出__all__和文件系统大小写不敏感有什么关系。

    正是前面这一点我没看懂,所以之前还有一点也没继续问下去。其实还有一个问题就是,你#48 提到:
    > 这个时间点上,如果 Python 决定重写 import hook ,将版本号纳入成为包名的一部分,支持安装同一个包的多个版本,就没有今天虚拟环境什么事了。
    为什么这样能解决需要虚拟环境的问题?按我所理解的,go 之所以能把所有 mod 都哦下载到$GOPATH/pkg 下,然后在加上版本区分,完全是因为二进制文件能够记录编译时的版本号(说的更完整一些,完全也可以静态编译)。

    而对于脚本型的语言,没有一个地方记录库对应的版本,因此这类语言采取的方式都是设置对应的 PATH (比如 PYTHONPATH 等等),然后在 PATH 中寻找最先遇到的库的版本导入。如果你要多版本共存,在同一个 PATH 下,遇到同个包的两个版本,该导入哪个?
    kuanat
        64
    kuanat  
       342 天前
    @cnt2ex #63

    我的描述是基于自己的记忆和理解,不一定就是正确的。

    我原本是想要通过 __all__ 这个例子来体现,基于文件系统的 import path 对今天包管理机制造成的困扰。

    在当时 windows/dos/Macintosh 的文件系统上,import * 遍历出来的文件名是大小写不固定的,也就导致了引入的符号表大小写不固定。于是设计了 __all__ 机制,因为 __all__ 定义的大小写是可控的。开发者可以选择 opt in 。

    __all__ 机制是需要手动维护的,也不能阻止用户显示地手动引入。如果今天重新设计一门语言,__all__ 是完全不必要的,可以通过 public/private 关键词,也可以通过某种 name convention 来实现。

    并不是说基于文件系统的 Package 组织形式不好,而是说这是一个选择。Python 选择了简洁,但是要在别的地方(包管理)付出代价。__all__ 机制就是这个代价之一。


    回到虚拟环境的问题上,现在假想一下 Python 依旧使用基于文件系统的 Package 组织形式,但是在路径名中包含版本号。

    现在 Python 就面临几种选择,要么以 import xyz==1.1.1 的形式使用,要么 Python 自己实现一个版本选择机制然后维持 import xyz 的向后兼容。

    第一种方案几乎没有人选的。因为一旦要升级依赖,需要显示地在源代码里面做修改。但是这个方案的改版非常多见,比如 npm 的 package.json 就是集中把 xyz==1.1.1 放到了一起。

    (看起来很完美对吧,但是当时没有人意识到这个 NP 问题会成为日后的麻烦。这种组织方式从机制上就不允许引入同一个包的不同版本,所以 NP 问题退化为 P 问题的途径就少了一个。)

    第二种方案,考虑到时间节点,不是一个好的选择。但这个路线是近十年来以软件工程为导向的语言的首选。

    这里需要明确区分 Package 的组织形式和 import path 的实现,前者是一种 specification 规范,后者是一个实现方式。Python 完全可以从规范层面要求所有的包名都带版本号,然后 import hook 的实现无视它,固定使用版本号最大或者最小,甚至文件最后修改信息最新或者最旧的版本。


    虚拟环境解决的是:在 Package 组织形式是基于文件系统、且 Package 规范不包含版本号的前提下,提供一种在同一个系统中,安装同一个包多个不同版本的可能性。

    如果 Package 规范要求包名带版本号,那么“安装”这个行为是不受限制的,也就不需要虚拟环境了。至于安装好了之后,使用时“选择“哪个版本,这是另一个事情。

    技术层面上,我猜 Python 转向 npm 类似的包管理方式是没有太大难度的,PEP 621 规范了 pyproject.toml ,也就是前面说的方案一,楼上 frostming 开发的 PDM 就是这么做的。现在的问题是 Python 的 import hook 本身并不支持路径名带版本号的,所以还是需要再套一层虚拟环境,让 Python 只能访问到特定的包。

    说得再具体一点,包管理器在文件系统里,以包名+版本号的形式,集中管理所有的包。虚拟环境指定了 $PATH ,在当前环境中的包软链接到实际的带版本号的包,然后软连接的名字不包含版本号。包管理器额外维护一个 pyproject.toml 记录当前所用的包,同时负责计算依赖。在此环境内的 Python 解释器和源码都不需要做任何改变,完全向后兼容。
    cnt2ex
        65
    cnt2ex  
       341 天前
    @kuanat #64

    > 如果 Package 规范要求包名带版本号,那么“安装”这个行为是不受限制的,也就不需要虚拟环境了。至于安装好了之后,使用时“选择“哪个版本,这是另一个事情。
    所以我对“如果 Python 决定重写 import hook ,将版本号纳入成为包名的一部分,支持安装同一个包的多个版本,就没有今天虚拟环境什么事了”这句话产生疑问。
    无论怎么重写 import hook ,又或者在包里加上版本区分。仅仅是在“被导入方”加入版本信息还不够,还需要在“导入方”记录要导入的版本才行。
    脚本型语言和编译型语言不一样,不存在编译、运行两个过程,编译型语言可以在编译时,在二进制文件里记录版本信息,那脚本型语言在哪里记录版本号就是个问题了。

    > Python 完全可以从规范层面要求所有的包名都带版本号,然后 import hook 的实现无视它,固定使用版本号最大或者最小,甚至文件最后修改信息最新或者最旧的版本。
    你所说的几种情况,一旦某个依赖的包升级,就有可能导致已有项目崩溃。且不说 lib.py.1.0 被 lib.py.2.0 取代,会导致依赖 lib.py.1.0 的项目崩溃。
    即使只是 lib.py.1.0.1 被 lib.py.1.0.1 取代,这种崩溃也是可能的,semver 无法解决这个问题。因为很多时候你无法区分 breaking, enhancement 和 bugfix 。
    用别人的例子, [你在你的包里加入了一个 warning ,这个是属于 breaking, enhancement 还是 bugfix ?]( https://twitter.com/brettsky/status/1262077534797041665)
    这里选择 bugfix 的是最多的,可你又如何保证你新输出的 warning message 不会 break 别人的项目呢?所以 semver 本身是无法被依赖的。你无论再怎么设计规范,总会出现你的 bugfix 成为别人的 breaking 的情况。

    其实我想说的重点是,多版本共存本身不是问题所在。反而由于如果强行多版本共存,在运行时,要如何“选择”哪个版本是主要问题。而为了解决这些问题,又进一步会带来一堆问题。

    所以,为什么非要多版本共存?没有多版本共存本身就是一种解决方案,而不是问题。
    cnt2ex
        66
    cnt2ex  
       341 天前
    > 即使只是 lib.py.1.0.1 被 lib.py.1.0.1 取代
    修正一下:即使只是 lib.py.1.0.0 被 lib.py.1.0.1 取代
    eailfly
        67
    eailfly  
       340 天前
    我到觉得 venv 这种方式更爽,新建项目的时候在本目录创建一个.venv ,不搞了直接一个 rm -rf 清爽干净,bundle 好是好,就是有时候试用或者玩玩一个 gem ,过后清理吧可能有依赖剩下,不清理吧一直放那也不用,也可能我还没领悟到 bundle 的精髓吧。。。
    scguanzhong
        68
    scguanzhong  
       276 天前
    我也觉得 venv 很清爽啊 ,不需要直接删除就 ok 了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2941 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 13:36 · PVG 21:36 · LAX 05:36 · JFK 08:36
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.