滚开吧,Python!

若要从本文汲取核心要义,那就是:大胆尝试“shebang”!它极具灵活性,能以创新方式实现代码快速执行。

你受够Python了吗?当某些LLM建议你用.mjs编写脚本时,你是否(理所当然地)想挖出自己的眼睛?现在有个解决方案:

//usr/local/go/bin/go run “$0” “$@”; exit

取一段脚本:

//usr/local/go/bin/go run “$0” “$@”; exit
package main
import “fmt”
func main() {
    fmt.Println(“Hello world”)
}

赋予执行权限 chmod +x script.go,现在你就能像运行可执行文件一样执行 ./script.go

❯ ./script.go 
Hello world
元素周期表

原理解析

经典的posix魔法。若咨询大型语言模型,它会归因于Shebangs。事实果真如此?让我们探究。

在Unix系统中,Shebang具有高度灵活性。其作用是告知操作系统在解析文本文件时应使用何种二进制程序。最常见的两种用法如下:

  1. #!/bin/bash
  2. #!/usr/bin/env bash

第一个示例告诉操作系统使用bash执行后续文件。我们声明:“请使用此解释器运行文本文件中的以下行”。
第二种方式本质相同,但存在微妙差异:我们使用env作为解释器,并传递参数bash,随后执行相同操作。此处的含义是:我们正在使用某个二进制文件并传递参数。

顺带一提,有人认为第二种方法能提高兼容性,因为我们利用env定位bash——它可能并不位于/bin/bash路径下。至于这种说法是否属实,我不敢妄加评论。

这不是shebang!

完全正确。本文开头的示例并未使用shebang。shebang 通过 execve 实现,其明确规定:

路径必须是二进制可执行文件,或以以下形式开头的脚本:

#!interpreter [optional-arg]

那么实际情况如何?让我们通过strace命令一探究竟:

❯ strace -f sh -c './script.go' 2>&1 | grep execve
execve("/usr/bin/sh", ["sh", "-c", "./script.go"], 0x7ffef82f3508 /* 102 vars */) = 0
[pid 2922314] execve("./script.go", ["./script.go"], 0x5a217a9eb7f8 /* 102 vars */) = -1 ENOEXEC (Exec format error)
[pid 2922314] execve("/bin/sh", ["/bin/sh", "./script.go"], 0x5a217a9eb7f8 /* 102 vars */ 
[pid 2922314] <... execve resumed>)     = 0
... (invocation of go itself, not interesting this post)

因此脚本由执行execve的shell解释。关键在于ENOEXEC(E-NO-EXEC,即执行权限错误)。让我们聚焦细节:

execve("/usr/bin/sh", ["sh", "-c", "./script.go"], ...) = 0
        ^^^^^^^^^^^^^^^^
        we started sh

[pid 2922314] execve("./script.go", ["./script.go"], ...) = -1 ENOEXEC
                    ^^^^^^^^^^^^^^
                    sh asks the kernel to run ./script.go *directly*
                    kernel: "Exec format error" (ENOEXEC) → not ELF, no #! shebang

[pid 2922314] execve("/bin/sh", ["/bin/sh", "./script.go"], ...) = 0
                    ^^^^^^^^^
                    sh sees ENOEXEC and falls back to: /bin/sh ./script.go

因此 script.go/bin/sh 执行,后者将文件按行解析为 shell 脚本。故此行代码:

//usr/local/go/bin/go run “$0” “$@”; exit

将由 /bin/sh 执行。你可能会疑惑”第一行不是注释了吗?为什么它会执行?“,答案在于 shell 不将 // 视为注释。在 sh 中尝试以下代码:

////////usr/local/go/bin/go

你会发现它能正常执行 go 命令。最后的 run “$0” “$@”; exit 将整个执行流程串联起来。

“$0” 传递参数 0。Arg0 就是二进制文件本身的路径。我们用 args.sh 脚本验证:

echo "Argument 0 is: $0"

执行结果:

❯ ./args.sh
Argument 0 is: ./args.sh

在本例中,被执行的二进制文件路径指向script.go,因此它能找到并构建自身。题外话:数月前因找不到 arg0 的实际用途而困惑时,我特意查证了其存在意义,最终发现了这个解答。困惑且对回复不满意的我,放弃了探究“为何存在arg0”这类历史遗留功能的尝试。如今却自然遇到了更合理的用例。世事真是妙不可言。

无论如何,这个“shebang”(它其实不是shebang)的完整命令是:

/usr/local/go/bin/go run “script.go” “$@”; exit

在命令行运行时,它会编译并执行script.go。由于“shebang”行被注释掉(//),Go编译器会跳过它。$@“展开为从1开始的定位参数”,即传递 arg1, arg2, ... , argn。这将支持类似 ./script.go -f flag0 here are some args 的调用方式。
; exit 部分是必需的,否则 sh 会将 Go 文件作为 shell 脚本逐行执行,并在找不到“变量”package时报错。

至此,我们完成了闭环。Go语言脚本功能现已上线!

为何如此?

这篇帖子最初纯属调侃,但越想越觉得并非糟糕的主意。Go语言作为编译型脚本语言具有独特优势。

脚本是实现自动化解决方案的捷径。我能在10秒内写出bash/python/lua等脚本,唯一区别在于shebang标记。这种便捷性的弊端在于它赋予的自由:极易演变成混乱局面。便捷性的代价是难以扩展,除非添加支持系统和严格规范。那么,如果将支持系统和规范直接内置到“脚本”语言本身呢?

Go脚本的核心优势在于其完善的标准库与兼容性保障。多数语言仅追求向后兼容,而Go将此特性内置于核心。只要使用Go 1.*版本,你编写的“Go脚本”永不失效——这正是企业环境的理想选择。

此外,兼容性保证极大简化了“脚本”共享流程。只要接收端安装了最新Go版本,该脚本未来数十年内可在任何操作系统上运行。
任何尝试在不同系统部署Python的人都深知其陡峭烦人的学习曲线。运行脚本本应简单到只需一句“执行此命令”。我不想折腾虚拟环境,也不想研究pippoetryuv的区别。这些都不重要,我只想运行代码。

Go生态内置的工具链是另一大卖点。我们无需依赖.pyprojectpackage.json来配置临时格式化工具和代码检查器,更不必通过管道确保一致性。只需运行默认Go环境,其工具链已深度集成至现代IDE。不过稍后我们会看到这同样存在弊端。

其他编译型语言

技术上“编译型语言脚本化”对其他语言同样可行,但据我所知没有哪种语言能像Go这样完美契合。Rust 编译速度较慢且标准库功能有限,这将迫使开发者依赖第三方库。而我主张通过不依赖第三方库、仅使用标准库来确保兼容性。此外(根据我的经验),Rust 对代码完美性的要求也导致开发效率稍低。

Java及类似JVM语言或许表现更佳,但相信已有基于JVM字节码的脚本语言实现了同等保障。轻量级的Kotlin脚本(无需绑定JVM项目)?我全力支持!

若要从本文汲取核心要义,那就是:大胆尝试“shebang”!它极具灵活性,能以创新方式实现代码快速执行。

核心难题

gopls。脚本首行必须无空格。以下方案可行:

//usr/local/go/bin/go run “$0” “$@”; exit

gopls不接受这种格式,因为它要求每个注释后必须跟空格(//example// example)。因此以下写法无效:

// usr/local/go/bin/go run “$0” “$@”; exit

如何解决?目前尚未找到优雅方案[0]。禁用格式化是种方法,但会丧失Go语言独特的实用价值。另一种方案是放弃伪shebang技巧,直接使用go run [脚本]。为简化操作可设置alias gr=go run别名,不过这多少削弱了核心意义。

后续将持续探索。新年快乐!

LK, 25-12


附录

0: 解决 go fmt 问题的方案!

如讨论帖所建议,将//替换为/**/即可规避格式化问题!因此今后脚本中我将采用:

/*usr/local/go/bin/go run "$0" "$@"; exit; */
package main

import "fmt"

func main() {
	fmt.Println("Hello world")
}

注意exit;末尾的分号,缺少此分号将导致程序无法正常运行。

元素周期表抱枕

本文由 TecHug 分享,英文原文及文中图片来自 Go away, Python!

共有{313}精彩评论

    1. 实际上还有更优方案:

        #!/usr/bin/env -S uv run --python 3.14 --script
      

      这样甚至无需预装Python。uv会自动安装指定版本的Python并执行命令。

      1. 此外,uv还支持以下写法:

          #!/usr/bin/env -S uv run --script
          #
          # /// 脚本
          # requires-python = “>=3.12”
          # dependencies = [“foo”]
          # ///
        
        1. /// 脚本块实际上在 PEP 723 中有明确规定,除 uv 外还有其他工具支持该特性。

          1. 上次我在这里评论称赞 uv 的优点时,也收到类似回复,指出 PEP 723 规范了这种行为,uv 并非唯一方案。因此我在此帖再次尝试:我看好 uv,并期待 Cunningham 的加入。

            1. 我也全力支持uv,并在日常工作中大力推广。但我认为将这些特性尽可能纳入标准对生态系统有益。或许几年后会出现比uv更优的方案,而在此期间,标准化能加速其在编辑器语法高亮等场景的普及。

          2. 这消息很棒;你知道还有哪些工具支持它吗?

            1. 据我所知,Hatch、PDM、pipx和pip-run也支持该功能。

        2. 我已着手将近十五年积累的临时Python脚本全部迁移至新格式。目前仅在使用时手动更新,总想着若能熟练运用grep/sed/正则表达式等工具,就尝试系统性批量更新所有.py文件。但多数脚本未纳入Git版本控制,散落在各自服务过的目录中。我曾数次尝试创建“Python脚本仪表盘”或“临时工具协调器”,却总在意识到这些脚本彼此无关且使用频率极低时放弃。我持续关注相关讨论,觉得这本该是Codex或其他智能工具的轻松任务。但这些Py文件属于“我个人所有”(编写时我清楚它们的运作逻辑),且分散在各处——我绝不可能放任智能工具在我的文件系统中肆意操作。

          ^多数情况如此,部分定义可能来自StackOverflow的复制粘贴

          1. 你可以在文件系统根目录运行ripgrep快速定位大部分脚本,速度快得惊人,然后将其作为输入给Claude之类工具生成自动化脚本。

        3. 这是快速开发的神级功能。

          虽然文档肯定强调过——但请务必谨记:若你被这种简易初始化方式吸引而创建Python项目,切勿在测试/生产环境使用此代码。

          若你尚未理解其不适配生产环境的原因,核心在于:当项目或其依赖项采用此方法时,创建可部署的构建产物将导致无法实现可重现构建。

          这还会导致构建在CI环境通过却在目标环境失败,反之亦然——因为依赖项都是实时下载的。

          可能存在变通方案,但本人对此特性不甚了解,如有必要请自行研究。

          仅供参考。

        4. 这并非真正的“替代方案”,而是指出除shebang外,您还可添加PEP 723依赖项规范,该规范可被uv run(如pipx及其他工具)所识别。

      2. 没错,但你需要uv。既然要使用可能缺失的工具,不妨依赖nix-shell:

            #! /usr/bin/env nix-shell
            #! nix-shell -i python3 --packages python3
        
        1. 没错,但你需要Nix。如果要调用可能缺失的工具,也可以依赖curl | sudo bash在缺失时安装Nix。

          (开个玩笑)

          1. 没错,但你需要curl、sudo和bash…

            1. “给我190字节的x86汇编十六进制种子,我便能编译整个世界。” ——阿基米德

            2. 它根本没解决任何问题,因为原本就不存在问题。整个 uv 项目就是在为不存在的问题找解决方案。

            3. pip 和 venv。Python 生态圈如今鼓吹必须在 venv 中操作一切的做法,简直是倒退。当我需要随时随地从所有终端调用可安装的实用脚本时,这种要求根本行不通。

              我理解将软件包安装到site-packages会带来安全隐患。但安装到我的主目录不会,为什么不能默认采用这种方案?Debian曾通过dist-packages分区实现简易管理,将site-packages作为安全沙箱,可惜后来妥协了。

              1. 关于为何不能用家目录:你安装的是项目A需要的Foo版本,还是项目B需要的不兼容版本?

                虚拟环境的精妙之处在于A和B能拥有完全独立且互不相容的环境。

                1. 它们自有其用武之地。但默认设置不该在用户需要通用适用性时强迫你进入“项目”模式。Python在命令行中的运行体验应当如二十年前般顺畅,而非莫名破坏既有功能却不提供低门槛替代方案。

                2. 为何不能像npm/gradle/maven那样管理依赖?Python有何特殊之处?

                  1. Python的虚拟环境本质上只是更复杂的node_modules。PDM、Poetry和uv这类工具能自动管理依赖,其效果已与npm无异。
                    Python的独特之处在于它从未设计过项目级隔离机制,而这套方案是目前最有效的语言行为改造方案。

              2. 多年来,pipx几乎满足了我安全运行实用脚本的所有需求。

                uv已取代它成为我的首选,并取代了我生产环境中编写的(少量)Python代码所使用的其他工具。

        2. uv不仅完全取代了pip、pyenv和venv的所有功能,更在它们的核心功能上表现得远胜于它们,同时还提供了诸多便捷、简单且对开发者友好的特性。

          1. Python糟糕的包管理并非全因pip所致——distutils和setuptools曾给我们带来setup.py的种种麻烦——但无论如何, UV彻底摒弃了这些缺陷,采用现代化的PEP 508清单规范——该规范具备一致性、声明式特性且可解析,同时搭配精心设计的锁定文件(UV诞生时尚无公认的锁定文件规范,随着PEP 715获采纳,UV已添加支持,但该规范仍存在局限,后续需进一步完善)。

          2. pyenv 运行良好,但 uv 速度更快且通过 uvx 提供了更多实用功能

          3. venv 一直令人头疼——需确保始终处于正确环境、支持 shell 操作等。uv 则隐形自动处理这些问题——因为它本身就是工具,无需担心在正确环境运行 pip 等操作。

            1. 我理解这句话是“uv建立在多年PEP规范之上”,这没错;但`uv`的用户体验是其独特之处,它显著减少了我思考需求、模块等问题的耗时。

              1. 既然如此,那就把这个原型移植回Python并弃用uv吧。

            2. 他们确实尚未实现,但正朝着这个方向努力。

        3. > 据我所知,Python工具无法跨项目共享数据,导致相同的大型依赖项会被重复构建多次。

          uv最精妙的功能之一在于它运用了巧妙的符号链接技巧——即使存在十几个不同Python环境都依赖相同组件,磁盘上也仅存放该组件的单一副本。

          1. 准确说是硬链接。实现并不复杂,基本只需用Rust版的os.link替换os.copy即可。真正巧妙之处在于包缓存中实际存储了可复用的文件,而非每次都从头解压轮子包。

            要实现此功能,pip首先需合理组织缓存结构,使其真正成为下载缓存。当前它本质是HTTP缓存(本地构建的轮子除外),通过第三方库模拟连接files.pythonhosted.org(常见PyPI场景)。但它仍需连接pypi.org来获取第三方库模拟访问的URI地址。

        4. 这部分源于Python庞大的用户基数及其易用性(包括分发机制)。这种特性导致用户更易遇到问题,从而催生出众多替代解决方案。

          不像Rust这类语言,用户基数小得多(虽在增长),还得有编译器诅咒学和词法诠释学的博士学位。

          或是C++,虽然安装基数庞大,却完全没有标准分发机制,还得有背部炮术的荣誉学位。

        5. 若非uv的存在,我绝不会忍受Python。它就是这么出色。

          在uv出现前,我已开始用Go语言编写原本该用Python实现的代码。

          1. 作为一名主要使用Java的开发者(自2001年起),我曾远离Python许久。最近两个工作项目都采用Python开发,且在我加入时都已切换至uv。这在开发周期和痛苦程度上形成了巨大差异——我完全赞同你的观点。

            Python始终是相当友好的开发语言,而uv使其成为最令人愉悦的开发体验之一。

            1. 我甚至不喜欢Python这门语言(虽然逐渐接受它了,但程度有限)。

              它就是如此实用:uv框架卓越,且几乎所有需求都能找到质量上乘的包。

        6. uv解决了问题,现在可以安全回归了。

      3. 过去12-15年间确实发生了哲学层面的转变:最初以解释器为核心,最终演变为通过生态管理工具为每个项目配置专属解释器(及虚拟环境等)。

        我认为这一转变真正始于RVM的诞生。当时Ruby解释器正经历不兼容的变更,主流发行版的版本滞后,而Rails(人们转向Ruby的主要原因)对支持的解释器版本要求相当严格,这些因素共同催生了RVM。此外,构建能成功运行Rails的解释器并非易事——虽非极其困难,但足够让便捷封装工具变得必要。于是整整一代Web开发者在这样的环境中成长:核心语言并非他们的初始接触点,且没人认为可以(或应该)依赖基础操作系统中通过apt-get安装的软件。

        这总体而言是极好的事。

        但RVM的关键贡献在于打破了问题的核心循环依赖:它本身并不依赖于已有的Ruby解释器。在此之前,人们对那些非用目标语言实现的工具总带着某种优越感,但RVM解决了足够多的痛点,直接冲破了这种壁垒。

        随后其他语言也涌现出类似工具——nvm和leiningen是首当其冲的例子,但这里还应加入asdf等工具——它们用于环境配置的可执行文件都带有’#!/bin/bash’ shebang行。

        Go语言之所以能规避这些问题,源于三大特性:1) 严格的向后兼容性;2) 极简的安装入口;3) 与上述发展进程的时间差——除非用户主动安装,否则操作系统极少预装go二进制文件。而Python在这些方面均未达标。这段时期的向后兼容性破坏堪称传奇,系统几乎总会预装 Python 版本造成混乱,而安装新版本时又必须避免破坏操作系统依赖的旧版本——这本身就存在风险。再加上我提到的挑剔性(至今在 uv 线程中仍可见),Python 如今才在追赶其他语言十年前就实现的功能。

        再次强调。

        1. 若我们刻意歪曲视角,“先构建生态系统管理工具,再考虑解释器”的思路竟与…包管理器的逻辑惊人相似,哈哈。

      4. > 若你从未接触过Clojure却要启动Clojure项目,几乎肯定会收到建议让你使用Leiningen。

        我以为Clojure当前的最佳实践是使用闪亮的新内置工具?deps.edn之类的东西?

        1. Clojure CLI(即deps.edn)发布于2018年,在“如何管理依赖项”的调查中,其使用率于2020年初突破50%。至今已有6-8年历史。

        2. deps.edn确正在成为默认选择。我理解楼上评论的意思是:“你会看到建议使用leiningen(尽管存在更新的解决方案,仅仅因为在文章撰写时它曾是默认选择)”

      5. 你认为非Python用户能否通过shebang行推断出应使用何种工具?

      6. uv问世还不到两年。它正稳步成为默认选择,只是时间问题。

      7. 你可能会看到有人推荐使用uv,但也可能推荐venv、poetry或hatch。

        这就像说“你可能会听到有人说开福特,但其实可能指的是内燃机,也可能是日产或现代”。

        1. 这只对深谙Python的人有意义。对局外人而言,这些词汇同样毫无描述性且随意,甚至连区分组件与工具品牌的专有名词首字母大写规则都不明显。

          1. 最令我恼火的是,人们抱怨时根本不愿探究底层原理——最终结论竟是:在开源项目中多人并行解决问题,这居然成了某种“坏事”。

            1. 这尤其讽刺,毕竟Python哲学里有条“实现方式应当唯一且最好只有一种”。可为何Python的外部环境反而像从Perl动物园逃出来的怪兽?

              1. 因为许多人根本不懂软件打包或如何编写兼容性良好的程序——那种能像普通应用程序那样正常安装的软件。我怀疑他们大多先在 node.js 或 ruby 生态系统里学过东西,结果就成了这样。就像要求用 docker 安装或构建应用程序一样,这既不酷、也不有趣,更不是正确的做法。我至今不明白venv究竟哪里不好,竟让大家需要uv。我根本没必要尝试——毕竟写Python代码的时间长到无法估量。在我看来这纯粹是为重写而重写,还非得用Rust实现不可。如果它真那么优秀,好吧,我明白了,或许确实如此——但所有优秀特性都该回归Python本身。

              2. 最根本的解决方案在于底层的virtualenv抽象层。其他所有实现不过是让这个核心功能更便捷或更透明。

              3. 赞同kstrauser的观点。

                但更详细地说:它看似复杂是因为

                * 人们拒绝学习许多资源都清晰阐释的基础概念;例如https://chriswarrick.com/blog/2018/09/04/python-virtual-envi… [0]。

                * 人们执着于早已过时的旧问题。当有人引用XKCD 1987时,却忽略Python 2.x已终止支持近六年(3.6版也超四年,但无所谓)[1]; 只有Mac用户需要担心Python的“homebrew”(据我所知当年它会直接干扰系统)或“框架构建”;easy_install同样是早已被弃用的恐龙*——一旦配置好pip你就永远用不上它*;而真正需要Anaconda的人正越来越少[2][3]。

                * 实现方式从来不止一种,这取决于你对“实现”的理解。每个人总会设想底层功能能被包装得更友好,而关于何为最友好的方案,他们又会产生多种相互矛盾的构想。

                但确实存在一种“显而易见”的实现方式:先创建虚拟环境,再运行该环境的Python可执行文件。其他所有操作都只是表面功夫。所谓“激活”环境,本质上只是配置环境变量,使python命令指向虚拟环境的Python可执行文件。所有替代工具本质上只是提供不同方式来确保运行正确版本的Python(我猜是基于你不想记住路径的假设),并把虚拟环境创建与其他开发任务打包在一起。

                Python社区明确鼓励多人提供此类封装工具,但并非通过制定“第十五个竞争标准”实现,而是通过提供标准规范(实际上是一套协同工作的标准:标准库中的虚拟环境支持、描述pyproject.toml的PEP规范等),从而取代了混乱局面(当时Setuptools是“警长”,pip是其“副手”)。

                [0]:顺带一提,本文作者本人并不喜欢虚拟环境,且是PEP 582提案的最大支持者之一。

                [1]:当然这不能怪兰德尔·门罗。漫画创作于2018年,正值社区努力梳理混乱局面、探索如何避免每个项目(包括纯Python项目)都依赖问题频出的setup.py配置的关键时期。

                [2]: SciPy软件栈长期以来几乎所有人都能通过轮子安装,即便在标准库移除distutils导致受限的情况下,他们仍能迅速发布3.12版本轮子。

                [3]: 至于真正需要它的人,通常完全可以在该环境中生存。

        2. 我认为此处特指直接使用python -m venv命令行接口,而非通过其他封装工具操作。

          1. 合理。

            我的教学方式是这样:从基础开始,这样你总能将其作为后备方案,同时更深入地理解系统。

            我通常将用户分为两类:真正需要学习这些知识(且能从中获益)的求知者,以及只求代码能运行的纯终端用户(开发者若想吸引这类用户,就应提供相应支持)。

    2. 2019年我用PyFlow解决了这个问题,但无人问津,便失去了兴趣。这是个用Rust编写的开源工具,能自动透明地管理Python版本和虚拟环境。只需配置pyproject.toml文件,运行pyflow main.py等命令,就能直接运行。它能像Cargo那样安装并锁定依赖项,还能为项目安装并运行正确的Python版本。

      当时Poetry和Pipenv很流行,但我觉得它们不够完善——虽然在抽象依赖方面做得不错,却没能解决虚拟环境和Python版本的问题。

      1. 听起来很棒。出于好奇,你认为为何pyflow未能流行,而UV却成功了?

        1. 我的推测是:我营销能力欠佳,且过早放弃。收到的反馈多是“既然Pip、Pipenv和Poetry都好用,何必用这个?” 在我看来它们并不完美——因无法处理虚拟环境和Py版本而麻烦重重,但当时没发现多少人遇到同样问题。

        2. 关键在于uv能自动获取完整Python解释器,无需编译或手动安装。

          回过头看,这正是rye曾短暂吸引用户的原因。

    3. 我也基本转用uv了,必要时用uv pip,但主要还是用uv add。不过一旦开始用uv pip,就会遭遇它的所有弊端——即后续传递的参数会影响先前依赖项的解析结果。执行uv pip install dep-a后再执行... dep-b,其效果不同于先执行... dep-b再执行... dep-a,也不同于uv pip install dep-a dep-b——对于习惯了规范依赖解析和工作区环境的开发者而言,这种差异极易造成困惑。

      不过这更多是 pip 的问题而非 uv 的问题,我个人仍倾向使用 uv pip。但 Python 包管理似乎注定混乱不堪,连 uv 这种临时解决方案都无法彻底解决这类问题。

      1. 我离开 Python 领域已有一段时间,原以为 uv 能解决依赖地狱问题。使用 uv/pip 组合有什么优势?速度?

        1. 据我观察,单独的pip甚至无法完成基础操作——比如先解析依赖树再并行下载所有包。而uv pip适配层能做到。

          无论你使用纯 uv、uv 包装的 pip 还是原生 pip,后期安装的依赖都会覆盖前期安装的依赖。目前似乎没有工具试图解决这个问题,因此我认为这是 Python 本身的缺陷,而非包管理器的缺陷。

        2. uv pip本质上仍是 uv,只是 uv 为 pip 提供的兼容层。

      2. uv让我很沮丧。它究竟想解决什么问题?它既不是虚拟环境管理工具,却又兼具此功能。我猜它本是依赖管理工具,但这种定位很奇怪。我认真尝试过,但最终还是靠shell函数绕过它的限制。

        最终我还是回归了经典的virtualenvwrapper.sh和手动设置PYTHONPATH。这样能完全掌控虚拟环境的构建方式。不过我理解人们喜欢开发新工具的心情。

        1. uv确实解决了许多问题,但最典型的场景是运行某些基于Python的命令行工具时:1/系统未安装任何Python解释器;2/操作系统自带的Python解释器与工具不兼容; 3/你希望直接从任意文件夹运行单个或多个工具(数据已存在于该文件夹),而非调整工作流程以适应虚拟环境,或冒着两个工具依赖冲突导致虚拟环境失效的风险。

        2. 或许我“进入”Python生态的时机特殊,但从未使用过virtualenvwrapper.sh,也从未手动设置PYTHONPATH。初接触Python时,当时推荐的做法是执行virtualenv venv && source venv/bin/activate。后来我改用python -m venv,但始终配合piprequirements.txt使用。这种模式持续到大约一年前我开始尝试uv,现在我仅使用uv中的uv venv|pip|init|add命令,不再依赖其他工具,主要处理基础操作。

          对于更复杂的项目和使用场景可能难度更大,但它比单纯使用pip快得多,而且pyproject.toml比requirements.txt更易于管理——这两点优势就足以让我轻松迁移。

    4. 你甚至无需指定首选的Python版本,uv会自动下载。

    5. 这样依赖项不就变成全局安装了吗?可能会引发冲突吧?

      1. uv使用全局缓存,但会将脚本依赖硬链接到专属临时虚拟环境中,因此速度依然很快。

      2. 不会!uv会处理好这些。uv堪称艺术品。

        1. 那我真该好好研究下它了。原以为它只是个普通的包管理器。

    6. 关于这点我实在有太多困惑。

      首先,在我认知中,所谓“脚本”不应依赖标准库之外的组件,即便有依赖也应高度契合个人系统需求。(值得注意的是,作者提及Go在此领域的优势之一正是标准库能避免快速脚本依赖外部库!这难道不是Python自诞生之初的核心卖点吗?)

      其次,即便存在依赖,也无需钻研不同工具的差异。选定一种即可使用。

      第三,虚拟环境本质上只是磁盘上用于安装依赖的空间,包含配置文件和标准库提供的单行命令自动创建的占位文件。若无意深入,完全不必进入其中检查任何内容。你不必使用激活脚本,若愿意也可直接指定venv的可执行文件。这些操作在概念上都不复杂。

      第四,对于这些快速脚本,共享环境实际上绝大多数时候都能正常工作。在养成规范管理习惯前,我多年都靠这种方式过得去,现在也通常能应付(不过为当前项目创建独立环境确实是最确保依赖项正确列出的方式)。根据我的经验,临时脚本根本不会遇到数组库版本冲突之类的问题。

      …说真的,为了避免考虑动态依赖问题就要转用编译型语言?若真这么妙,当初谁还会发明Python这类语言呢。

      还有啊…

      > 只要接收端安装了最新版Go,这个脚本未来几十年在任何操作系统上都能运行。任何尝试在不同系统上运行Python的人都知道这有多么令人头疼。

      这里的伪shebang技巧在Windows上同样行不通。而且不,我从Windows转用Linux时,Python环境的配置根本不算什么“令人头疼的陡峭曲线”——随着对Linux整体环境的适应,这些配置几乎是自动完成的。

      (我猜用“.pyproject”代替实际有效的pyproject.toml纯属故意挑刺。)

      1. > 第三,虚拟环境本质上只是磁盘上存放依赖项的空间

        最近和同事聊起uv的便利性,对方却说幸好自己讨厌虚拟环境,现在转投TypeScript了。我问他node_modules是什么,他沉默片刻后承认“你说的对”。

        uv依然使用虚拟环境,因为这是Python官方将所有项目包集中存储的方式。Node/npm、Go/go和Rust/cargo都做类似的事,但我只听人抱怨Python的版本管理——正如你所说,这完全可以被忽略,永远不必关注。

        1. 据我观察,很多抱怨源于人们不喜欢“激活脚本”工作流,却误以为这是强制要求。当然也有人纯粹出于审美抗拒——毕竟这个环境库并非简单的site-packages文件夹,而是具有内部结构(好吧;那该如何告知Python使用它?)

          关于PEP 582的超长讨论(https://discuss.python.org/t/pep-582-python-local-packages-d…) 讨论PEP 582(https://peps.python.org/pep-0582/; 即“__pypackages__”文件夹提案)在此似乎具有相关性。

          1. 我也听过这些反对意见。确实有人抱怨这又增加了一步操作。不过像direnv和mise这类工具能解决这个问题。我个人喜欢这种激活流程的明确性——你明确激活了特定的虚拟环境,或者根据需要切换到其他位置的虚拟环境。我不喜欢到处散落“uv run …”的代码。但妙处在于两种方案都可行,你可以任选其一。

            很期待看到__pypackages__及其相关工具如何演进。

            1. > 但值得称道的是两种方式都可行,你可以选择偏好的方案。

              没错。pyenv方案同样可行(据我理解是将相对路径永久添加到$PATH环境变量中,系统会在该路径放置一个调用当前工作目录关联虚拟环境的存根可执行文件)。

              手动基于子shell的方法等等也同样可行。

              在“开发模式”下我使用基于激活脚本的封装工具。而临时编写代码时,通常直接显式指定虚拟环境的python路径:

              1. 我将你的“临时编写”方法用于cron任务等场景,命令行如下:

                        • /path/to/project/.venv/python /path/to/project/foo.py
                
                虽然需要多输入一次,但能避免后续大量脆弱性问题。
                
                
    7. ….但必须能获取UV库,某些平台(如树莓派)因Rust版本过旧无法编译。于是我用Python写了名为“pv”的脚本,功能类似uv——仅够让程序运行。虽然有点好笑,但它在任何环境都管用,完全满足我的需求。我只需嵌入一个由AI生成的原始TOML解析器即可。

      1. > 我只需嵌入一个由AI生成的原始TOML解析器即可。

        对此的标准推荐方案是`tomli`,它在3.11版本中成为标准库`tomllib`的基础。

  1. Go语言明确拒绝添加shebang支持,强制要求使用此类技巧,因其被视为“资源滥用”[0]。相反,Pike也称之为错误的`gorun`被推荐使用。该方法可进行修改,避免硬编码路径。

    /// 2>/dev/null ; gorun “$0” “$@” ; exit $?
    
    
    >经典的posix魔法。若询问大型语言模型,它会归因于shebang机制。
    
    ChatGPT给出的解释与本文一致,考虑到该机制已被反复验证,这并不意外。
    
    >没有其他语言能像Go这样完美契合
    
    Nim、Zig、D语言均支持`-run`参数实现类似功能。Swift、OCaml、Haskell可直接执行文件,无需额外参数。
    
    [0]: [https://groups.google.com/d/msg/golang-nuts/iGHWoUQFHjg/_dbL...](https://groups.google.com/d/msg/golang-nuts/iGHWoUQFHjg/_dbLKomrPmUJ)
    
    
  2. >我不想搞虚拟环境,也不想研究pip、poetry和uv的区别。我才不在乎这些。我只想运行代码。

    所以这其实是技能问题,博客文章里提到的所有问题,`uv run`和PEP 723都解决了。

    1. 虽然这是事实,但`uv run`的出现耗费了如此长时间,这常常令我震惊。

      我断断续续使用Python超过20年,始终厌恶处理任何涉及外部包或虚拟环境的代码库。

      `uv run`彻底改变了这种状况,我在上份工作中将所有代码库都迁移至此。可惜个人项目为时已晚——我早已将它们转换或直接用Go重写。

      我对Python的长期前景持观望态度。我始终偏好类型化语言,而随着LLM辅助编码的兴起,类型一致性变得尤为重要。

      1. 说得好。我也正处于对Python犹豫不决的状态,过去受过太多挫折。

        即便uv能完美解决所有痛点,它在打包部署方面仍逊于那些自带官方工具的语言。

        给猪涂口红终究是徒劳…

    2. 任何历史悠久的语言都存在竞争性库,长远来看你终究得学习它们。

      1. 库文件,没错。但涉及打包/构建/管理运行时环境的工具链?我持保留态度。Perl使用CPAN已有二十年之久,我认为这个生态系统绝非“唯一解决方案”的典范。我觉得你的推论方向有误:老语言确实较少配备官方工具链,因此更可能存在多种实现方式,但我不认为有充分证据表明,那些最初就配备官方工具链的语言必然延续这种趋势。Python的问题与其说是历史积淀,不如说是其工具链长期表现欠佳。即便官方工具存在缺陷,只要出现更优替代方案,开发者自然会选择替换。但我认为,当官方工具链足够完善时,人们终将停止折腾。

        Go语言在此确实具有示范意义:它最初仅靠`go get`和广泛的vendoring机制支撑,随后涌现出各种替代方案(如混淆性极强的godep和dep),但官方模块管理工具最终确立主导地位。时至今日,几乎所有人都已放弃过渡工具,统一采用官方工具。这恰恰证明:即便早期缺乏统一工具,只要建立规范化的官方认证流程,工具泛滥的局面终可遏止。

    3. 这是用户体验问题。作者说得对——*没人关心*那些玄乎的虚拟环境或其他技术术语。

      用户

      运行

      该死的程序。

      > `uv run` 加上PEP 723就解决了作者描述的所有问题。

      PEP 723啊?“决议日期:2024年1月8日”

      当然啦,只要你神奇地掌握了uv的使用方法,就能享受整整两年的正常基础体验。太棒了,Python生态万岁!

      uv是官方推荐的Python默认运行方式吗?不是?那就等着和那些放弃这门语言的用户挥手告别吧。

      1. 我不明白你的观点。那些连`uv run`都打不出来的用户,更别说输入`//usr/local/go/bin/go run “$0” “$@”; exit`了。这两种方式都不是“官方推荐的默认运行脚本”。

        强烈建议你先读完文章了解上下文再评论——我猜你就是这么做的。

      2. > uv是官方推荐的Python默认运行方式吗?

        是的,它似乎已成为事实上的默认方案,也是官方推荐方式。

      3. > *uv是官方推荐的Python默认运行方式吗?不是?那就准备向所有放弃该语言的用户挥手告别吧。*

        uv基本上是昨天才诞生的。但它成为默认工具的速度比我见过的任何工具都快。

        所以我认为:是的——它现在实际上就是默认选项。

      4. > 用户只是想运行那个该死的程序。

        我不认同,用户希望按自己期望的方式运行程序,但当无法实现时就会感到沮丧。

        若所有依赖项都已安装在机器上,脚本本应能顺利运行。我有些脚本的依赖项就直接安装在系统中。

        作者写道:

        > Go生态系统内置的工具是另一大卖点。我们无需依赖.pyproject或package.json来配置临时格式化与代码检查器,更无需管道支持来确保一致性。

        难道shebang不是解决此问题的方案?它虽能便捷地将脚本作为可执行文件运行,但环境配置本应由用户负责。随后他继续阐述Go拥有强大的标准库,使其成为脚本编写的理想选择。这正是我通常选择Python处理复杂脚本的原因——其标准库足以解决多数问题。

        如今Node内置sqlite后选择不再那么简单,但即便需要手动配置环境确保脚本运行,我也不会对Node和JavaScript心生怨言。我理解其运行机制和依赖获取方式——若忘记在运行脚本前执行`npm i`,那便是我的失误。比起神秘莫测的魔法,我更偏爱能提醒我愚蠢的错误提示。

  3. 这简直是疯狂天才的杰作。

    不过…根据我的经验,脚本编写与可交付软件需要不同的设计理念。具体差异难以言表,但bash极具脚本化特性,Go语言则更适合交付,Python居中,Ruby接近bash,Rust则与Go同样偏向交付端。

    优秀的脚本编写是操作系统级构造与当前语法环境的融合(显然bash只是通过语法糖包装操作系统命令来实现条件语句、循环和变量),同时适用于那些无需复杂工具链的场景:依赖管理、测试覆盖率之类的东西。那些鼓励快速、粗糙、即弃式代码的语言,才能让我完成销售人员周四急需的临时任务,从而顺利完成月度收尾工作。

    Go语言却并非如此。若用Go开发,我总想同步编写测试用例,总想搭建完善的构建管道,总想建立发布流程。

    我从未如此深入思考过语言的人体工学特性,很想听听大家的看法。

    1. 说到Python“居中而立”——昨晚我演示了一个简单的WebView GTK应用,本想在原生Debian环境运行… 于是按月度标准流程,用uv创建虚拟环境并拉取依赖。尝试运行代码时却陷入混乱——错误提示环境配置正确却仍无法运行(?),最终Python核心转储崩溃。好吧。每次我尝试用Python实现新想法时,总会以某种形式遭遇这种情况。虽然Go语言更冗长(我也不太喜欢mod.go系统),但一旦编译成功,程序就能直接运行,无需尝试运行或依赖各种操作系统特有的黑科技。

      1. Gtk会让简单的Python程序变得极其复杂,因为它需要纯Python依赖之外的组件。

        这确实是Python的巨大痛点。纯Python依赖的使用体验极佳,但大量包依赖于需要编译的C扩展或存在操作系统依赖。虽然wheels和multilinux构建有所改善,但依然很容易自掘坟墓。

      2. 我确信 Astral 并未构建 GTK 依赖项,这意味着它未必总能正常工作——毕竟他们采用的 Python 构建方式实在…独特。数月前我在 uv 环境下运行 Tkinter 项目时遇到类似问题,后来改用 conda 才顺利解决。

      3. 依赖项是如何指定的?用于初始化虚拟环境的文件是什么类型?

      4. 我用Anaconda时没遇到过这种问题,建议试试看。

        1. 我以前也遇到过类似的Anaconda问题。有次遇到关键障碍,搞砸了我一整天的工作——除了基础的venv+requirements.txt组合外,其他所有Python依赖/环境工具都让我碰壁。虽然这种组合干扰最小,但帮助也很有限,你只能依赖requirements.txt文件,而管理它往往容易出错。

    2. 我明白你的意思。

      对我而言,关键在于语言表达的紧凑性——具体来说就是能否用单个文件完成任务。

      毫无疑问,许多Go项目完全能用500行脚本搞定。但Go语言更侧重于多文件模块协作,用于设计用户自定义类型、多线程等功能。这些对BASH而言都不成问题,而Python自带的原生类型已足以完成多数任务,无需定制类型。

      若你的Go脚本需要整目录代码才能运行,还不如直接编译后安装到/usr/local/bin。

      原生Go不支持bang-line的事实,暗示着大家都在“做错事”。事实上,`go run` 命令本质上只是将代码编译为临时二进制文件,这进一步印证了这种做法的合理性。

      1. 我认为在满足以下约束条件的前提下,Go语言仍存在开发替代脚本语法的空间:

        * 单文件暴露“main”包及其“func main()”
        * 导入语法整合go.mod中的依赖要求 (同时支持包和模块导入)

        * 简化错误处理(代码中忽略返回错误,转译器会将其捕获为致命错误)

        这些构想旨在超越我现有的 goeval 方案——该方案已支持运行 Go 单行命令。[https://github.com/dolmen-go/goeval](https://github.com/dolmen-go/goeval)

    3. > bash本质上只是用语法糖包装操作系统命令

      不,从技术层面看,bash并非比Python“更接近操作系统”。它只是恰好成为终端模拟器的默认shell而已。

      1. 我不同意这种说法。从技术层面看确实如此,两者都是解释型语言,但执行某些操作时的人体工学体验和认知开销却天差地别:

        在Python中,数学运算或复杂字符串/集合操作通常只需一行代码,但调用shell命令或其他操作系统进程却需要折腾subprocess模块、编写临时流式循环等——更别提将多个命令串联起来了。

        Bash则截然相反:只要任务能拆解为一系列shell命令,它便能大放异彩——但一旦需要任何形式的自定义数据操作,你就会遭遇棘手的边界情况和任意限制,即便在其他语言中属于基础操作的功能也不例外。

        1. > 在 Python 中,…调用 shell 命令或其他操作系统进程需要折腾 subprocess 模块、编写临时流式循环等操作——更别提将多个命令串联起来了。

          你启发我快速整合了更简洁的方案——[https://pypi.org/project/shell-pilot/](https://pypi.org/project/shell-pilot/)

        2. subprocess模块确实糟糕透顶,但即便它很优秀,bash依然更简洁。我只是在思考如何在Python中创建进程管道而不引发阻塞风险。

      2. 我热爱Python却厌恶Bash,但看看Bash和Python列出文件夹的差异就知道了。

        1. 没错。用Python检测其他用户是否登录。获取每个分区易于阅读的剩余空间。

          这并非Python做不到,但比起直接在终端输入<10个字符,实现起来要复杂得多。

    4. 若能快速让大型语言模型(LLM)代劳代码编辑,编写过程的体验问题或许就没那么突出?我们可以转而优化代码可读性。

      更具体地说,是优化LLM生成的代码可读性。

  4. > *我不想搞虚拟环境,也不想研究pip、Poetry和uv的区别。这些我都不关心。我只想运行代码。*

    作者说得对,每个接触Python的用户都不该被迫搞懂这些。但我强烈认为,每个打算撰写此类观点文章的博主,至少应该在动笔前尝试理解这些概念。

    显而易见的无知无法支撑有力论点。尤其当uv能解决你描述的所有问题时。

    1. > 尤其当uv能解决你描述的所有问题时。

      uv如何像Go那样解决“写一次,到处运行”的问题?

      (我并非讽刺,只是对uv了解有限,或许缺乏理解这种机制的思维模型)

      1. > *UV如何像Go那样解决“写一次,到处运行”的问题?*

        以下是UV官网[0]上的示例Python脚本:

        #!/usr/bin/env -S uv run –script

        /// 脚本

        requires-python = “>=3.12”

        dependencies = [“httpx”]

        ///

        import httpx

        print(httpx.get(“https://example.com”))

        
        在任何安装了uv的系统上,这段代码会自动在脚本级虚拟环境中安装Python 3.12(或更高版本)及pypi的httpx包,并立即执行脚本。
        
        [0] [https://docs.astral.sh/uv/guides/scripts/#using-a-shebang-to...](https://docs.astral.sh/uv/guides/scripts/#using-a-shebang-to-create-an-executable-file)
        
        
      2. UV是pip的替代方案,而非替代品。它在后台运作机制不同于pip,因此速度更快,但使用时本质上仍是pip——只不过让一切变得更简单。创建虚拟环境用 uv venv,更新包用 uv sync,无需激活环境直接运行程序用 uv run…它采用 pyproject.toml 配置文件,因此共享代码检查工具和构建工具变得极其简单。部署时,可将pyproject.toml编译为requirements.txt文件,适用于任何Python容器镜像。这在使用Azure容器函数等不支持UV的环境时尤为便利(毕竟你既不想使用UV镜像,也不愿在构建流程中安装UV)。

        我使用它已久到最近当BI团队需要帮助时,竟想不起没有它该如何编写Python代码——这实在令人尴尬至极。

        不过我认为它仍不及Go语言。若需跨不同Python发行版开发,其操作复杂度远高于Go;在微型Python等场景下的适配性,也远不如Go针对特定嵌入式平台的精准性。

  5. 我其实不太理解最初的动机。我*喜欢*用Python编写脚本——这正是它的优势所在。你可以极快地写出简单脚本完成任务,无需纠结类型、内存之类的问题。但我*不喜欢*用Python作为大型应用的主语言。

    1. 我也热爱用Python编写脚本。只是厌恶安装他人脚本的过程。

      1. > 讨厌安装别人的脚本。

        这句话听起来自相矛盾。脚本的本质就是无需安装(除了标准解释器),直接*运行*即可。

        1. 按这个逻辑,你根本不需要*安装*操作系统,只需将引导程序和其他辅助文件放到你选择的存储介质上运行即可

      2. > 我就是讨厌安装别人的脚本。

        这种观念对我来说依然很奇怪。大概就是…与我对“脚本”这个词的理解不符吧。

        1. 你竟不理解人们运行他人编写的软件这个概念?

          我对Python最大的困扰恰恰源于Freecad大量使用Python开发——Python3会在脚本执行路径遍地创建_pycache_目录(这意味着所有位置,包括我所有Git仓库内部), 因此我不得不把_pycache_加入所有.gitignore文件),而本该禁用这种愚蠢行为的环境变量毫无作用——因为FreeCAD是AppImage格式,我的环境变量无法传递到它自行创建的环境中。

          这就是我“尝试安装他人脚本”的真实写照,那个“他人脚本”不过是个叫FreeCAD的小玩意儿,没什么大不了的。

          1. > 这就是我“尝试安装别人的脚本”的经历,那个“别人”的脚本不过是个叫FreeCAD的小玩意儿,没什么大不了的。

            我不明白的是,你为何称其为“脚本”。

            > 而且python3会在脚本执行过的每个位置写入_pycache_目录(这意味着遍布所有地方,包括我所有git仓库内部,所以我不得不把_pycache_加入所有.gitignore文件)

            这本就是预期行为;许多来源(包括GitHub)提供的标准“Python项目” .gitignore文件里都包含这条规则。

            但你的意思是仓库里存放着FreeCAD会导入的插件?否则实在想不通它为何要执行你仓库内的.py文件。

            总之这似乎是场离题的抱怨。本质上这和Java生成.class文件没什么区别——我很少见到有人为此如此困扰。

        1. 但这样就需要依赖uv

          可移植性就差了

          1. 内联脚本元数据本身并非uv专属,它是Python标准特性。两者关联源于用户通过uv接触ISM,以及二者同步发展的历史背景。

            pipx可运行带内联脚本元数据的Python脚本。该工具以Python实现,并被Linux发行版、Free/Net/OpenBSD、Homebrew、MacPorts及Scoop(Windows)打包支持:[https://repology.org/project/pipx/versions](https://repology.org/project/pipx/versions)。

            1. 是的,许多场景都可使用内联脚本元数据。

              但脚本仅能包含一条shebang指令。

                1. 我感觉其他人似乎没真正理解你/原帖作者的思路。你的意思是用户应本地配置机器,确保标准化名称指向能解决问题的工具,并接受该选择带来的特殊性,对吗?

                  很多人描述的PEP 723用例中,接收者可能根本不知道Python是什么(或如何检查兼容版本),但可以被指导安装uv后复制运行脚本。这种想法确实会增加该用例的操作摩擦。不过我认为在这种情况下,你其实应该直接打包独立运行程序(使用PyInstaller、pex、Briefcase或其他无数选项)。

                  1. > 你的意思是用户应本地配置机器,确保标准化名称指向能解决问题的方案,并接受该选择带来的特殊性,对吗?

                    在阅读论坛讨论和Stephen Rosen的评论前,我确实这么认为。现在我认为最有价值的元运行器应按顺序尝试常用运行器。

                    我已在[https://github.com/dbohdan/python-script-runner](https://github.com/dbohdan/python-script-runner)发布原型。

                    1. 不错。当然除非它成为标准并随Python发行,否则价值有限 😉 但我认同你的思路。或许值得重启那个讨论帖来探讨此事。

    2. 这似乎是Linux专属方案(在其他类Unix系统上是否运行?)而Linux通常自带系统级Python,其稳定性足以满足脚本需求,但此方案需要额外安装Go语言环境。

      你也可以使用Shell脚本、Python或其他脚本语言。虽然Python在向后兼容性方面表现不佳,但大多数脚本几乎不会遇到问题。Shell脚本具有向后兼容性,许多其他脚本语言(如TCL)也具有很强的向后兼容性,且更可能预装在系统中。如果你正在安装Go语言,只需安装uv并使用Python即可。

      文章确实提到“我最初发帖主要是为了挑刺”,这确实是部分原因,但主要动机是你对Go语言有着强烈的偏好。

    3. 我实在不明白JS作为脚本语言有何不足。

      blabla

      node bla.js

      1. 并非差,但Python开箱即用更完善:Toml格式、csv支持、真正的多线程(自3.13起)、基础GUI、更优异的REPL(原生卓越且可安装IPython)、argparse等众多特性。

    4. 类型问题始终需要关注。你必须清楚函数返回值的类型及其可操作性。

      当你精通语言时,基础类型的信息无需查阅——因为你早已铭记于心。

      但这同样适用于强类型语言。

      1. 在脚本环境中,Python对此的重视程度远超表面意义——它不会像某些脚本语言那样进行类型强制转换。例如若要连接整数与字符串,必须先将整数转换为字符串类型。它还内置了多种类列表和类字典的类型,这些类型之间不可互换。相较于其他脚本语言,Python 更需要开发者“关注类型问题”。

    5. Python 对程序员而言堪称瑰宝,对其他人则是邪恶垃圾。

      若你关心除自己以外的任何人,请勿用 Python 编写供他人分发、安装、集成、运行或长期使用的代码。

      若你只顾自己,尽情享受Python吧。

  6. 本以为是抱怨,竟收获人生真知。足以成就一个快乐的新年。

    话说回来,任何将`//`视为注释的语言都能滥用此技巧。

    实用(?)语言清单:C/C++、Java、JavaScript、Rust、Swift、Kotlin、ObjC、D、F#、GLSL/HLSL、Groovy

    个人认为GLSL最有趣。单行GLSL图形演示总能带来灵感(类似[https://www.shadertoy.com/](https://www.shadertoy.com/))。

    别忘了还能用块注释(`/* … */`)实现类似效果。C语言示例:

    /*/../usr/bin/env gcc “$0” “$@”; ./a.out; rm -vf a.out; exit; */

    #include <stdio.h>

    int main() { printf(“Hello World!\n”); return 0; }

    1. Swift甚至存在一个项目[1],支持运行具有外部依赖的脚本(因上游项目基本停更,故分享分支版本)。

      我认为这是uv在Swift领域的对应方案。

      (值得一提的是,Swift脚本还原生支持真正的shebang机制。)

      [1] [https://github.com/xcode-actions/swift-sh](https://github.com/xcode-actions/swift-sh)

    2. C/C++只需使用“#!”。TCC初问世时,我们正是用此技术实现“C脚本编写”。这需要采用肮脏的SO方法(无法有效控制链接过程)。

      对于大型项目(可执行文件),shebang指向C语言构建文件,该文件编译时可获取根路径;随后C构建脚本查找清单文件,执行构建、链接并调用fork()。优秀的a/m时间戳库若直接支持ccache,即使在大型项目中也能实现与脚本同等的启动速度。

      重申:这套方案存在根本缺陷——环境控制难度极高。

      记得我们是在2000年代中期搞这些操作?TCC编译器是什么时候问世的?

    3. Rust语言无需如此滥用注释,因为它直接支持shebang机制。

  7. 若追求更…符合人体工学的语言,也可使用.NET 10新增的“直接运行文件”功能。它直接支持shebang,甚至能自动安装脚本中引用的包!

    #!/usr/bin/env dotnet run
    #:package Newtonsoft.Json@13.0.3

    using Newtonsoft.Json;

    Console.WriteLine(
    JsonConvert.SerializeObject(new { Hello = “world” })
    );

    
    更妙的是,借助 #:sdk 指令,您甚至能直接从“花哨的shell脚本”中部署微型Web应用...
    
    

    #!/usr/bin/env dotnet run
    #:sdk Microsoft.NET.Sdk.Web

    WebApplication
    .Create()
    .MapGet(“/”, () => “Hello from a shell script!”)
    .Run();

    
    
    1. 今天我写了第一个此类脚本(为C#代码库中的代理工具开发)。体验相当不错,不过AOT编译确实还有些粗糙之处。

  8. 我原以为这会是一篇更长的抱怨,说Python该…消失。作为一名资深的Python程序员和贡献者,曾经还是该语言的狂热拥护者,我倒乐意听听这个建议。我认为将所有机器学习都用Python实现是个巨大的错误,我们将为此付出多年代价。

    主要原因在于它运行缓慢,类型系统比其他语言复杂得多,且难以部署。使用它的唯一理由就是惯性使然。固然惯性有时确实合理,但我希望业界能将Python列为最后选择,转而将TypeScript、Go或Rust(视具体场景而定)视为最佳实践。Python应被视为过时技术,仅用于PyTorch等现有代码库。谁会用Python写Web应用?类型系统糟糕透顶,运行缓慢,有太多更优选择。

    1. 完全赞同。

      话虽如此…机器学习领域选择Python确有其因。GPU编程需要基于C的库支持。NodeJS的FFI实现不够完善,Rust和Go同样如此。虽然存在替代方案,但Python的FFI支持在此场景下更胜一筹。Zig在此领域尚不成熟。

      世界需要一种类似Python的语言:拥有更完善的类型系统、更优的分发机制,且大幅减少动态特性带来的陷阱/自杀式设计。

      1. > 世界需要一种类似Python的语言:拥有更完善的类型系统、更优的分发机制,且大幅减少动态特性带来的陷阱/自杀式设计。

        Nim。不过工具链仍不够成熟。

      2. C#/.Net?(尽管它们过度执着于毫无价值的向后兼容性,且基础语言特性开发速度极其缓慢。)

        1. 坦白说我已有数年未使用C#,但据我所知它比Java更符合人体工学,个人更倾向于使用它。唯一阻碍我更广泛使用它的因素是其社区规模远小于Java/Python等语言。想听听你认为它缺少什么。

      3. > NodeJS的FFI支持并不出色,Rust和Go同样如此。虽然存在支持,但Python的FFI支持在这方面确实更胜一筹。

        咦。我发现Rust的FFI体验相当顺畅。我知道Zig在这方面无人能及,但Python在此领域究竟提供了什么Rust(或Go)所不具备的功能?

    2. TypeScript?

      为什么要用JavaScript衍生语言取代Python这样优秀的语言?

      1. 它的类型系统远胜Python,还具备Python缺乏的基本特性如代码块作用域。函数式编程在Python里也因有限的lambda表达式而刻意受限。

        若TypeScript能拥有强大的Python标准库和Numpy/机器学习生态,我绝对会毫不犹豫地弃用Python转投它。

        1. TypeScript的性能也显著更优。这主要得益于过去二十年浏览器大战催生了海量JavaScript引擎研发投入。Node.js运行V8引擎(即Chrome使用的JavaScript引擎),而Bun框架采用为Safari开发的JSC引擎。

          对于I/O密集型任务,JavaScript更简洁的线程模型同样具有优势,其内置的事件驱动I/O系统也极具竞争力。

        2. Python确实支持定义命名闭包,我偶尔会用到,不过似乎常让他人感到意外。可能这种用法不太常见吧。

      2. 尽管运行时环境不够完善,TypeScript仍是极佳的语言。真希望有个TypeScript子集能编译为Go之类的语言。

        1. 这不正是Project Corsa要解决的问题吗?

          1. Project Corsa是用Go重写TypeScript编译器的项目,我认为这并非当前讨论的需求。

      3. TypeScript在Web领域无处不在,还有一些惊艳的新框架能复用服务器端和客户端的TypeScript类型(如trpc、tanstack)。它比Python更快,拥有符合人体工学的类型系统,以及庞大的社区+npm生态。Bun在运行时性能上实现了重大突破(Anthropic刚收购并用于Claude代码开发)。

        1. 你是不是同时写了这两句:

          > 使用它的唯一理由是惯性

          > TypeScript在网页领域无处不在

          🙂

          1. 这两点都是使用两种语言的合理理由。“唯一”(无论是否属实)才是论点的关键所在。这大致相当于说X的唯一优势在于它很流行,但Y同样流行且具备额外优势,因此Y优于X。无论前提是否成立,这都算得上一个有效的论证。

            1. 我并不反对,但若你声称“X”仅因“Y”而被使用,那么当你推销“Z”而非“X”时,或许不该以“Y”作为开场白 🙂

              1. 同意存在分歧。他正在论证Z在严格意义上优于X。

      4. TypeScript太棒了!

        虽如所有语言般存在缺陷,但它将众多高级编程语言概念普及给了大众!

      5. TypeScript在许多方面都比Python出色。尤其通过Deno运行时,更适合脚本编写(导入机制完全符合开发者期望!)。

        当然也有不足之处,比如Python的任意精度整数在脚本编写中显然更胜一筹。Python的列表推导语法虽然规则怪异,但确实相当优雅。

        但总体而言,Deno作为临时脚本编写工具远比Python更胜一筹。

          1. 我明白,但Python默认支持。在JavaScript中需手动启用且不够便捷。例如尝试从JSON加载64位整数。

            1. 我同意,但JSON规范将所有数字定义为64位浮点数,因此缺少bigint类型。JSON中任何其他数值类型都属于非标准实现。

              JavaScript本身完全支持大整数字面量,只需在数字后添加’n’前缀,例如0xffffffffffffffn。

              我希望能为JSON添加大量功能,比如注释、二进制数据块、日期格式以及整数/大整数支持。若能实现这些特性,操作体验将大幅提升。

              1. > JSON规范将所有数字定义为64位浮点数

                完全不是这样。规范并未对数字精度或大小施加任何限制。

    3. "> 我认为将整个机器学习领域置于Python环境是巨大错误,我们将为此付出多年代价。

      市场压力使然。早期机器学习框架采用Lisp语言,后来Torch转向Lua,但市场需求最终选择了Python——只因“它简单易用”,即便这意味着拼凑出粗糙的解决方案。

      Lisp在神经网络领域仍是最优选语言,其优势远超本文讨论范围,但配套工具链缺失。我正在开发这样的框架,尽管深知它难以被广泛采用。Python或许不够优雅高效,但它简单易用——这正是人们想要的。

      1. 哎,真好奇为何Lisp在机器学习领域缺乏配套工具链,明明早期框架都用Lisp实现。莫非是语言特性阻碍了广泛协作?

        1. 考虑到每天都有庞大的团队在协作维护海量Clojure代码库,我对此表示怀疑。Lisp工具链的缺失和Python的盛行更多源于惯性、低门槛和生态锁定。

      2. Lisp究竟缺了什么工具?若你分享过框架,我很想研究看看

        1. Lisp本身并不缺什么,它与AI/ML天生契合。真正需要追赶的是生态系统的工具链。
          代码尚未达到RC阶段,但预览版准备好后我定会发布Show HN。

    4. 尤其令人抓狂的是依赖地狱似乎已深植于Python文化。那些“天啊这个库只支持Python 3.10+且需搭配一堆随机版本的库(具体版本我们懒得告诉你)”的声明,搭配其依赖库“必须3.8.56z版本才能运行,但要是你多看它一眼就不行,还得说’拜托拜托’才行”的设定,简直令人发狂。显然,版本管理规范(Semver)也并非标准实践。

      我可能对Python存在偏见,所以请谨慎看待这个观点,但在我看来,整个生态系统就像一群业余软件开发者(尽管其中不乏专业的机器学习工程师、数据科学家等)拼凑出勉强能用的东西。

      我年纪足够大,还记得当年老派软件工程师们争相贬低JS和Node,指责其生态不成熟,并急于强调那不是“真正的”软件。但过去10-15年间,JS和Node似乎已整顿好局面,而Python在依赖管理和环境配置方面仍完全停留在2012年的水平。若向专业Python开发者询问此事,得到的回答总是:“哦,其实很简单,你肯定没花时间真正了解它,因为Python很简单,它就像伪代码一样,看看它多么神奇”。

      我真心希望机器学习领域没有将Python作为标准语言。作为机器学习工具和框架的用户而非全职工程师,这简直是无休止的折磨。

    5. 我渴望用某种简单、表达力强且强类型的语言替代Python,这种语言还能编译为原生代码。我习惯编写小型命令行工具来方便操作内部API,虽然你可能觉得Go和Python在这类场景中性能差异不大,但实际存在明显区别。用Go写这类工具一年后,我又回到了Python——代码行数差距实在太悬殊。但每次运行这些工具时,我仍希望它们是用Go写的。

      (OCaml或许符合我的需求,但我实在提不起劲去学它,毕竟面对这种源自学术界的二十世纪语言,光是工具链和依赖管理就令人望而生畏。)

      1. 你试过Nim吗?强类型静态编译,兼容性强,可编译为C原生代码,与C语言无缝交互,还提供宏等脑洞大开的特性(如果你喜欢的话),入门门槛极低。

        [https://nim-lang.org](https://nim-lang.org)

        1. 看起来很有意思。代码示例呈现出类似Python的简单面向对象/命令式风格。乍看之下,大量常用功能依赖宏的设计让我感到困惑,但似乎这是语言设计中用户不介意的一部分?或许值得尝试。

      2. 你可以用Nim替代Python。它完全符合你的所有要求(表达力强、运行快、编译型、强类型)。它和Python一样简洁,而且我认为Nim的语法更灵活。

        [https://nim-lang.org](https://nim-lang.org)

        1. 编译速度足够快,甚至能像脚本那样用shebang方法运行。

      3. 没错,Go语言几乎不能称为静态类型语言——他们到处使用空接口。

        OCaml确实值得研究,或许OxCaml更佳。Jane Street团队最近在工具链上投入了大量精力。

        1. > 是的,Go 很难称得上静态类型语言,毕竟他们到处使用空接口。

          你究竟多频繁地使用 *any*/*interface {}*?没错,有时这是解决问题的正确方案,但根据我的经验,这种情况其实并不常见。至少不会常见到真正影响开发效率的程度。

          自从泛型出现后,我对空接口的使用频率又进一步降低了。

      4. Rust或许值得关注。它在代码行数和便捷性上比Go更接近Python这类动态语言,同时拥有更完善的类型系统。还配备了完全现代化的工具链和依赖管理系统。当然还有原生代码支持。

      5. 几年前我曾因工具链问题放弃OCaml,尽管它几乎完全符合我的语言需求。如今我对Gleam非常满意,并推荐它在多数场景下替代OCaml。

        1. 考虑过F#吗?该语言与OCaml高度相似,但额外具备完善的工具链和庞大的包生态系统(可兼容所有.NET包)。

        2. 我一直认为专为高并发、容错、长运行进程设计的运行时必然存在显著启动开销——这正是Python令我困扰之处。Gleam是否也存在此问题?

          1. 今年我用Gleam参与了Advent of Code竞赛。启动时间差异显著:Python约13毫秒,Gleam则需120毫秒。

            若追求极致启动速度,需选用能编译为原生二进制的语言,如Zig、Rust或OCaml。

        3. Gleam能否用于临时脚本编写?我认为这需要满足多数语言难以达成的两项要求:

          1. 支持相对路径导入文件(Python不支持)
          2. 能在单文件脚本中指定第三方依赖,且*确保其与IDE正常兼容*

          目前我发现Deno是兼具这两项特性且支持静态类型的最佳选择。

          期待Rust未来也能实现,但至少还需要一两年时间。

      6. 或许可以尝试TypeScript,它能通过Node或Bun编译为单一二进制文件。Bun和Node都会对TS类型进行类型剥离,并能将命令行界面编译为单一可执行文件。Anthropic公司就是这样处理Claude代码的。

      7. 不妨试试Dart。它结构简洁、工具链强大,还能编译为原生代码。

        免责声明:我在谷歌负责Flutter开发。

    6. 我敢说只有Hacker News评论区的人在乎Python类型系统。现实中我从未接触过真正执着于此的开发者,而那些稍有关注的人似乎对类型提示就很满意了。

      1. 或许有些在乎的人转投了其他语言。

        而那些仍在抱怨的人,部分只是暂时受困于此。

        这就像幸存者偏差。思考那些永远没有回归者所经历的问题,才是富有成效的。

      2. 我们碰巧共事的人群,在所有软件工程师中构成一个极其偏颇的样本集。

        例如,我职业生涯中接触的几乎所有人都偏爱macOS和Linux。但整个软件工程子社区中仍有人坚守Windows阵营,对他们而言macOS不过是古早玩具。

        若你从未接触过重视类型系统的同行,这恰恰反映了你的职场环境与同事素养。我共事过许多工程师,他们视动态类型为忌讳——尤其在FAANG这类企业中。

        早在TypeScript、Node.js甚至《JavaScript精粹》问世之前,谷歌就开发了名为Closure的JavaScript编译器。这个编译器是用Java编写的,功能强大——但据我所知,其核心目的就是为JavaScript添加类型系统。为什么?因为谷歌人宁愿从零编写编译器,也不愿使用动态类型语言。我知道它曾用于开发早期Gmail版本,或许至今仍在使用。

    7. *我认为将所有机器学习都用Python实现是个巨大错误,我们将为此付出多年代价。*

      若机器学习能兑现其承诺,代码用何种语言编写根本无关紧要。

      若未能兑现,那也无所谓了。

    8. Python对机器学习的实际影响究竟有多大?所有库本质上都是封装GPU的C代码外壳,分布式计算和推理服务本就可以用更高效的语言实现?

      1. 你只考虑了最终的矩阵运算环节。Python在机器学习领域真正的价值在于*自动微分*。

        Python为此提供了多种*卓越*方案:JAX、PyTorch、TensorFlow、autograd等,每种库都适用于不同场景。

        我认为这些案例中,Python作为*语言*本身正是这些库诞生的原因(而如你所言,矩阵运算部分几乎任何语言都能实现这些C语言封装)。Python确实能轻松实现元编程,且在需要操作语言本身时具备极高灵活性。

    9. > 我认为将所有机器学习都置于Python环境是场灾难性错误,我们将为此付出多年代价。

      > 主要原因在于其运行缓慢、<省略>且难以分布式部署。

      别忘了Python相较于C语言消耗约70倍的计算资源。

      1. 这其实不适用于机器学习。运行在GPU上的海量计算并非用Python执行,其能耗本质上与宿主语言无关。

        1. 各类数据中心都配备大量非GPU硬件。若这些硬件用Python编程,其能耗将比C语言高出70倍。

    10. 这种态度既武断又狭隘。你试过FastAPI吗?

      1. 你说他思想狭隘,却只抓住他论述中最不相关的速度问题,还暗示带“fast”字样的工具就能解决?

        速度根本不是问题,因为像numpy这样的库是用C语言编写的,真正的开销在于粘合代码和ffi接口。缺乏标准分发系统才是大问题。动态类型对小型程序和团队很适用,但当规模扩大时就无法扩展。

        但纯Python因语言设计本质上就慢。除非引入语言约束(此时你面对的只是子集),否则无法高效编译。没有任何库能解决这个问题。

        1. 你所说的内容几乎与FastAPI无关——就速度而言,它与用Go编写的同类Web应用相差无几。你需要针对具体问题深入研究,而非做出笼统却不合时宜的假设。当前讨论的是Web应用领域,截至2025年底,Python在此领域仍具备强大竞争力——无论在速度、代码优雅性还是静态类型支持方面(FastAPI完全基于Pydantic实现)——[https://www.techempower.com/benchmarks/# section=test&runid=7…](https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-6)

        2. > 但纯Python因语言设计本质上运行缓慢。除非引入语言约束(此时你实际在处理其子集),否则无法高效编译。没有任何库能解决这个问题。

          前几天在另一个关于CPython的Python讨论中也有人提出类似观点,但我并不完全认同。诚然,这绝非易事。然而GraalVM已向我们展示了如何为Java泛型实现这一目标。Highover,只需获取应用程序,编译并运行即可。编译过程会处理泛型的字面量使用,运行时负责初始化类和内存,还可通过运行时插桩添加泛型的动态调用机制。显然要实现这些细节需要大量工作,但确实可行。

        1. 暗示其他编程语言中存在你偏好的工具,就将同等出色的工具贬低为“[巨大的]错误,我们将为此付出多年代价”、“纯粹出于惯性”,这种论调远低于我对Hacker News讨论水准的期待。

          1. 公平地说,你的评论也没提供多少实质内容。

            他们对Python的主要批评是:

            > 运行缓慢,类型系统比其他语言难用得多,且难以分发

            若能说明FastAPI如何解决这些问题,你的评论会更有价值。

            1. 若对方能像你描述那样措辞严谨,我本会认真回应并给予应有的尊重。但严重违背实质性讨论规范的评论,理应得不到精心构思的回应。

  9. > 核心问题在于gopls。脚本首行必须保持无空格…

    具体问题在于自动格式化。Gopls通常会在编辑保存时执行此操作,但CI系统应强制确保所有合并的*.go文件采用规范格式。这能确保修改者自行格式化代码(并为该行负责),而非让后续修改该文件其他位置的无辜者承担责任,同时还能减少合并冲突。

    但这种做法存在第二个(更严重)问题:一次性脚本无法使用 go.mod 文件,这意味着无法指定依赖版本,从而削弱了你发帖时强调的兼容性优势:

    > Go 脚本的主要优势在于[…]兼容性保障。多数语言追求向后兼容,而 Go 将此作为核心特性。只要使用 Go 1.* 版本,你编写的脚本就不会失效,这在企业环境中尤为理想。

    > 此外,兼容性保证极大简化了脚本共享。只要接收方拥有最新 Go 版本,该脚本未来数十年内可在任何操作系统上运行。

    1. 确实如此,但通过导入路径锁定的主要版本号理应保持兼容。

    2. > 这恰恰削弱了你发帖强调的兼容性优势

      未必?这涉及的是语言/核心运行时而非依赖项。

  10. 既然如此,何不也为C++或汇编语言实现同样机制?你如此憎恶脚本语言,宁可费尽心思使用编译型语言,抛弃脚本语言的所有优势乃至脚本编程本身——仅仅因为个人偏见而非技术价值。恭喜你,刚白白浪费了数小时宝贵时间。

    > 便利性的代价是难以扩展

    当然,它们永远无法扩展。当你开始考虑扩展性时,就该停止编写一次性脚本,转而进行规范化开发。这并非主张彻底抛弃Python或bash。如今将Python代码转换为Go的成本近乎为零——前提是确实需要转换。关于过早优化的讨论已足够充分。

    > 任何尝试在不同系统上运行Python的人都知道那有多么陡峭又恼人的学习曲线。

    若需调用十个特定版本的库才能运行几行Python代码,那已不能称之为脚本。它变成了需要正规包管理的正式项目——*就像Go语言一样*。

    1. Python与C++在语言易用性上的差距远大于Python与Go的差距。编译时间和包管理是C++的主要缺点。

      “宁可开紧凑型轿车也不要SUV?那干脆骑摩托车算了!”

    2. Python用于系统脚本的核心问题在于:即便在这个领域它也并非理想选择。

      Perl就在那里,无需安装,几乎存在于所有类Unix系统中。它确实不是好语言,性能不佳,扩展性差,但Python也一样,何必在意。况且它比Python更具表达力且更紧凑,或许甚至有些过头了。

  11. 我也写了一个!我决定不用 //,因为我的编辑器启用了 gofmt 自动格式化,它会在 // 和 usr 之间插入空格。这个格式不会被 gofmt 修改:

    /*?sr/bin/env go run “$0” “$@”; exit $? #*/
    
        /*?sr/bin/env go run “$0” “$@”; exit $? #*/
    
    1. 虽然能用,但我实在无法完全解释前三个符号的含义。/*?sr/bin/env 通过将 *? 展开为首个匹配目录来定位 /usr。但为什么不直接用 /*usr/ 呢?

  12. 我超爱这个方案。我正用Go处理全栈JavaScript应用的构建,效果相当出色——毕竟esbuild能直接嵌入Go程序运行。问题在于它属于依赖项,所以我妥协采用go mod文件并直接用Go运行。若能不配置显式模块(比如直接内联在Go文件中)就解决依赖问题就完美了。可惜这大概永远不会实现。

    话虽如此…用Go做脚本编写。效果超赞。如果不需要第三方库,这种方式显得非常简洁。

        1. 至少把我的名字拼对啊靠,组织架构图里明明写着

      1. 没错。没错,我全用JavaScript搞定 😛

        1. 我就是要当那种人。

          我让计算机完成任务,但从不自诩自己的工作是唯一推动事物发展的力量。庞大的软件栈中,我的工作只是最终环节。

          1. “全栈”这个术语有广为接受的共识定义,你这是在吹毛求疵

            1. 称之为“全栈”的问题(即便其含义广为人知)在于,它隐含地将从事底层工作的开发者置于神坛之上。这会给人一种错觉:既然这已是“全栈”范畴,那么设备驱动、操作系统或基础库之类的东西,必然是专家专属的玄妙魔法——而事实并非如此。

              “全栈”一词在其常规语境中尚可接受,但若放眼更广阔的领域,便会产生误导性,在我看来实属问题所在。

              1. 或者说,它无视并贬低了这些组件的存在价值。无论哪种情况,都对全栈体系中某条线以下的软件进行了奇怪的“他者化”处理。

            2. 对我而言并非如此,且我所在的计算亚文化圈也未使用类似的狭隘术语。

              1. >对我而言并非如此

                这无可厚非,但不意味着所有人都应如此。

                网页开发者区分后端、前端或兼顾两者已相当普遍(至少持续二十年)。其中“兼顾两者”几乎总被“全栈”取代。

                人们使用这类表述时,仅指其能处理网页应用的两部分,绝无贬低系统程序员或发电厂工程师之意。

              2. 你这小圈子的名词是从哪儿冒出来的,吹毛求疵镇?

                1. 主要指非网络软件,用编译型语言编写的

          2. 我认同你的观点——“全栈”这个术语既怪异又略显夸大其词。

            但它已在业界确立,强行反对恐怕难有成效。

    1. 这是显式支持而非使用相同的//技巧。该语言会刻意忽略shebang,即使它不符合常规注释语法。

      1. 确实如此,虽然少了些趣味性,但因其被官方支持而更具实用性。

    2. dotnet在这方面一直表现出色。自90年代起就有诸如toolsack等第三方工具实现此功能。

      我认为Java现在也能运行未编译的文本脚本了

  13. 我的兼容性方案是避免依赖第三方库,转而依托标准库

    有趣的是,这恰是Python在2005年等时期攻击Perl的核心论点。

  14. 这招很巧妙,我之前没见过。只要语言支持//注释,几乎都能实现。

    它确实依赖于//符号——根据POSIX规范,该符号的具体实现由系统定义。某些系统中//usr可能指向网络路径。

    最后一句:

    3.254 路径名

    用于标识文件的字符串。在 POSIX.1-2024 规范中,路径名长度可能受限于 {PATH_MAX} 字节(含终止空字节)。路径名可选地以 <斜杠> 字符开头,随后跟随零个或多个由 <斜杠> 分隔的文件名。路径名可选地包含一个或多个尾随的 <斜杠> 字符。连续多个<斜杠>字符视为单个<斜杠>,但开头恰为两个<斜杠>字符时是否特殊处理取决于具体实现。

    [IEEE Std 1003.1, 2024 Edition]

    语言设计上更优的选择是采用#注释,或在执行文件中特殊支持#!语法。这样也不必额外启动shell实例。(可惜这个//技巧无法使用“exec”命令将shell替换为Go程序。)

  15. 不久前我也以类似方式将所有构建部署脚本迁移至Go。实际收益在于服务使用的实用函数可在部署中复用。这样就能轻松共享代码来统一判断服务/数据库是否在线,或访问云端密钥。同时所有错误检查也变得清晰明了(curl失败是因离线还是格式错误)。

    结合Go模块使用时更显威力。让每个脚本调用共享“scripts”模块中的单一函数,所有脚本就能对称地从任意位置调用。这确保所有脚本即使不常运行也能正常构建。同时意味着任何脚本都能调用 scripts.DeployService(…) 函数,无需关心所在目录或调用者身份。参数机制清晰定义了每个脚本所需的路径/配置。

  16. 可移除末尾丑陋的“exit”并调整空格格式

        // 2>/dev/null; exec go run “$0” “$@”
    
  17. 我从未遇到过虚拟环境的配置或使用问题。甚至觉得没必要了解uv是什么,因为这对我而言根本不是问题。

  18. > 题外话:几个月前因找不到arg0的实际用例而特意查证,发现了这个答案。

    我认为arg0始终很有价值,尤其在开发像busybox这类多功能应用时——它能根据执行时的名称改变行为模式。

  19. 妙招!我曾无谓地琢磨能否让它在Ruby中运行——其实勉强可行,前提是能忍受脚本执行前的单条错误提示(可惜#注释无效,因为shell会将其视为注释):

        =begin
        ruby $0; exit
        =end
    
        puts “Hello from Ruby”
    

    虽然现在用不上,但这个技巧将来某个随机时刻肯定会派上用场。下面这个C99基础用法也很简单,不过我不确定是否愿意用它写脚本(!):

        //usr/bin/cc $0 && ./a.out && exit
    
  20. 早年见过类似用法:C文件被即时编译为临时文件后直接运行。

    例如 //usr/bin/gcc -o main “$0”; ./main “$@”; exit

    1. Tcc甚至支持通过#!/usr/local/bin/tcc -run实现,不过我不理解为何有人选择C或Go进行“脚本编写”——Python、Ruby、TCL或Perl在易用性上都远胜一筹。

      1. 这是个相对老旧的项目,它用C程序作为构建系统/元数据生成器。你只需一个能用的C编译器(以及执行首行的shell)。它会构建并运行一个生成各类表格和源代码的程序,随后编译实际程序。最终程序使用运行时反射系统,该系统由第一阶段生成的表格和代码构建而成。

        核心设计理念是仅依赖C编译器和POSIX标准库实现全部功能。

  21. 使用shebang时甚至无需文件后缀为.go,任何像样的编辑器都能解析shebang识别文件类型(…至少Emacs对我来说足够好用)

    程序命名完全不必拘泥于 foo.go,直接命名为 foo 即可

    1. go run 工具不会执行(甚至识别)非 .go 结尾的文件,因此此建议并不妥当。

  22. 哈哈,我刚用Rust尝试了同样的操作:

      //$HOME/.cargo/bin/rustc “$0” && ${0%.rs} “$@” ; exit
      
      use std::env;
      
      fn main() {
          println!(“hello, world!”);
          for arg in env::args() {
              println!(“arg: {arg}”);
          }
      }
    

    完全是权宜之计,生成的可执行文件会把./目录弄得乱七八糟。不过挺可爱的。

  23. > 不想搞虚拟环境,也不想搞清楚pip、poetry和uv的区别

    拜托,这很简单:

    项目里有 setup.py 吗?如果有,先执行几个命令才能运行它:python -m venv .venv && source .venv/bin/activate && pip install -e .

    否则项目里有 requirements.txt 吗?如果有:python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt

    否则是否存在pyproject.toml?若有则执行poetry install,后续所有命令需添加前缀poetry run …

    否则是否存在pipfile?若有则执行pipenv install,后续所有命令需添加前缀pipenv run …

    否则是否存在environment.yml?若有则执行conda env create -f environment.yml,查看文件内容后执行conda activate <环境名称>

    否则是否存在 uv.ock 文件?若存在,执行 uv sync(或 uv pip install -e .),随后所有命令需添加 uv run 前缀。

    1. > 得了吧,这很简单:(讽刺)

      若你检出仓库或解压 tarball 时未附文档,确实如此。

      但若从PyPI获取或文档允许,你完全可以自由选择工具。

      此外,pip+venv方案与pyproject.toml兼容性良好——该协议本就为互操作性设计。而Poetry侧重个人开发,不适用于协作项目。

      说到这,一个项目若同时存在pipfile、environment.yml、uv.lock等文件 却未配置pyproject.toml,这根本不算严肃的项目分发。若是团队内部项目,你们本就该清楚该怎么做。

      1. 每当你搬出“苏格兰人谬误”这种论调,就等于告诉我该立刻掉头逃跑。

        1. 指出大量项目被上传至GitHub等平台时,根本不关心他人能否实际下载、本地“安装”并使用代码,同时指望生态系统凭空解决这些问题——这并非“苏格兰人谬误”。既然Python生态系统存在且人们理解其开发规范,那么打包标准本就清晰明确且有据可循。

          将那些使用自定义工具(及其关联配置,通过配置可推断工具本身)的项目视为合法发行版——尤其当这些工具甚至未被宣传为终端用户包管理器时——实属不诚实;而将这种现象归咎于Python生态的缺陷,更是荒谬至极。任何人都能创建npm或Cargo的替代方案。

  24. 建议:将exit改为exit $?,这样退出代码就能传回给shell。

  25. 不得不贴出这段怪胎代码——它能让你用uv运行python脚本,或者在uv未安装时直接用python运行(给某些同事用的)

    #!/usr/bin/env bash

    “”“:”

    if command -v uv > /dev/null

    then exec uv run –script “$0” “$@”

    else

    exec python3 “$0” “$@”

    fi

    “:”“”

      1. 没有,只是觉得将注释斜杠与路径前导斜杠分开更简洁。

    1. 引用相关博客原文:

      此言是否属实,恕我不敢妄加评论。

      1. 该博客指的是env查找bash的情况。我理解其并未对env查找Go作出相同论断。bash通常位于/bin/bash(或存在指向该路径的符号链接),因其广泛应用于脚本,且该路径是公认的兼容性要求。Go语言则没有固定标准路径,我个人多年来在不同路径安装过Go(多数情况下env都能正常工作)。虽然我认同博主关于env查找bash可能改善兼容性的观点,但也赞同父级评论中关于env查找Go确实能提升兼容性的看法。

  26. > 当某些大型语言模型建议你用 .mjs 编写脚本时,你是否(理所当然地)想把眼睛挖出来?

    我对此观点不敢苟同。JS 是替代 Python 编写脚本的绝佳选择。Node.js 提供了各类实用函数,让你无需依赖外部库就能编写脚本。Bun、Deno和Node.js均可执行TS文件(若需引入类型系统)。这三种运行时性能均足够出色。即便最终需要外部依赖,只需一个package.json即可解决。如今我所有脚本都用JS文件编写。

  27. 请问…

    augroup fix autocmd! autocmd BufWritePost *.go if getline(1) =~# ‘^// usr/bin/’ | call setline(1, substitute(getline(1), ‘^// ’, ‘//’, ‘’)) | silent! write | endif augroup END

  28. 我尝试过Go脚本,但仍更倾向于Python(说实话uv确实改变了游戏规则)。不过自动化工具我首选始终是PowerShell(在Linux上)。可惜PowerShell带着微软的污名,让人们望而却步。如果你愿意,我可以劝你试试看

  29. 我一直想把某些点文件工具移植到Go语言,看来可以试试这个了。

  30. 我最讨厌Python可执行文件的一点——至少在Debian/Ubuntu里安装的那些——就是/usr/bin目录下的文件都是包装器,实际调用的是site-packages目录里的某个文件。

    我只想在执行位置看到完整脚本。

      1. 幸好你问了,我刚弄坏了这台Ubuntu实例:

            root@t:~# cat $(which scapy)
            ++ which scapy
            + cat /usr/bin/scapy
            #!/usr/bin/python3
            # EASY-INSTALL-ENTRY-SCRIPT: ‘scapy==2.5.0’,'console_scripts',‘scapy’
            import re
            import sys
            
            # 为兼容 easy_install;参见 #2198
            __requires__ = ‘scapy==2.5.0’
            
            try:
                from importlib.metadata import distribution
            except ImportError:
                try:
                    from importlib_metadata import distribution
                except ImportError:
                    from pkg_resources import load_entry_point
            
            
            def importlib_load_entry_point(spec, group, name):
                dist_name, _, _ = spec.partition(‘==’)
                matches = (
                    entry_point
                    for entry_point in distribution(dist_name).entry_points
                    if entry_point.group == group and entry_point.name == name
                )
                return next(matches).load()
            
            
            globals().setdefault(‘load_entry_point’, importlib_load_entry_point)
            
            
            if __name__ == ‘__main__’:
                sys.argv[0] = re.sub(r'(-script.pyw?|.exe)?$‘, ’', sys.argv[0])
                sys.exit(load_entry_point(‘scapy==2.5.0’, ‘console_scripts’, ‘scapy’)())
        
        1. 明白了。

          我理解你的观点,但这在Python中很常见。比如使用Poetry(https://python-poetry.org/docs/pyproject/#scripts)或Uv(https://docs.astral.sh/uv/concepts/projects/config/# command-…) 或 setuptools (https://setuptools.pypa.io/en/latest/userguide/entry_point.h…) 来为你的包定义“脚本”。这些工具会自动生成类似你在此处发布的文件,该文件基本会导入包并调用其内部的特定函数。

          几乎所有有价值的Python脚本都会导入其他模块,因此将整个程序代码集中在一个文件中本就相当罕见。这些自动生成的脚本只是将这种模式进一步深化。

  31. 若需另一种卓越的脚本解决方案,它具备:

    • 快速启动(无需编译)
    • 采用真正的编程语言
    • 轻松升级超越脚本层面
    • 内置海量优质依赖库

    babashka正是您的不二之选!这款Clojure解释器提供顶级脚本支持,内置强大库可实现:调用外部程序、文件管理、HTTP相关操作(客户端/服务器端)、文本解析、HTML构建等功能。

    Babashka 现已成为我启动所有新项目的首选工具。它几乎包含了所需的一切,即便存在缺失,其依赖管理机制也堪称我所见过的运行时中最灵活有趣的方案。通过“pod协议”,任何进程(无论采用Go/Rust/Java等语言编写)都能作为Babashka依赖项暴露并直接打包集成。无需额外执行“安装依赖项”命令,它会按需自动安装并缓存所需组件。

    当然,REPL开发模式的全部魔力依然保留。内置的nrepl支持只需在命令中添加’—nrepl-server 7888’参数,即可通过编辑器实时连接并编辑进程。我正用这种方式构建个人网站,体验简直太棒了。

    抱歉说了这么多,但遇到如此出色的脚本解决方案时,我必须分享对bb的喜爱。它实在太优秀了,不分享简直对不起它!!

  32. 特此说明:核心技巧在C和Java中同样可行。C语言版本已在相关评论中提及,而Java版本则需要更多基于bash替换的“技巧”。

    记得大学一年级时(十多年了!时间过得真快…)我曾构建过这样的Java“解释器”,因为入门课程总要求编写一次性程序,而主要语言是Java。

    虽然原始源代码已遗失,但大致是这样的:

    ///usr/bin/env javac $0 && java ${0%%.java}; exit; # /

    由于实现很简单,我甚至在$PATH里放了个名为“java-script”(双关语)的包装脚本,这样只需写

    //usr/bin/env java-script # /

    置于代码开头。可见我当时就用这种绝妙的命名方案把同学搞糊涂了 🙂

  33. 可使用https://github.com/erning/gorun作为Go脚本运行器。它支持在Go脚本中嵌入go.modgo.sum文件,实现依赖管理。相比Python的内联脚本元数据,这种方式更冗长且需要手动管理校验和。gorun会缓存已编译的二进制文件,因此脚本首次运行后启动速度很快。

    示例:

      #! /usr/bin/env gorun
      //
      // go.mod >>>
      // module foo
      // go 1.22
      // require github.com/fatih/color v1.16.0
      // require github.com/mattn/go-colorable v0.1.13
      // require github.com/mattn/go-isatty v0.0.20
      // require golang.org/x/sys v0.14.0
      // <<< go.mod
      //
      // go.sum >>>
      // github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
      // github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
      // github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
      // github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
      // github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
      // github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
      // github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
      // golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
      // golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
      // golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
      // golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
      // <<< go.sum
    
      package main
    
      import “github.com/fatih/color”
    
      func main() {
          color.Green(“Hello, world!”)
      }
    

    为兼容标准Go工具链,shebang行可替换为:

      /// 2>/dev/null ; gorun “$0” “$@” ; exit $?
      //
      // go.mod >>>
      // ...
    
    1. 我找这种方案找了好几次!太棒了

  34. 太棒了。这意味着Go仓库里的脚本也能用Go语言编写,而非Bash/Python。甚至能导入项目中的库文件。

    超赞!

  35. 我试用时被安全系统拦截了——基于行为的安全规则触发了警报,因为它看起来像信息窃取程序…

  36. > 题外话:几个月前我查阅了 arg0 的存在意义,因为始终找不到任何用例,后来发现了这个答案[0]。困惑且不满于这些回复,我放弃了探究“为何存在arg0”的努力,将其视为某种历史遗留功能。

    我实在难以想象这些答案还能如何更清晰或更令人满意。既然研究如此敷衍,何必撰写文章?更不该提及这个毫无价值的题外话…(诱饵?)

    [0] https://stackoverflow.com/questions/24678056/linux-exec-func

  37. Go语言不适合廉价脚本的根本原因在于:错误处理机制。

    它擅长编写“健壮”代码,而非那些默认崩溃也无妨的快速方案。

  38. > 这篇帖子开头纯属挑衅

    所以你的目标就是浪费读者时间。谢了。

  39. 题外话…我其实挺喜欢Python语言本身。但讨厌它管理环境的方式。

    这是我一贯的观点,尤其在语言和运行时领域更显重要:在操作系统或系统上安装软件的理念必须淘汰。

    Go、Rust等语言采用的“按工作区/按包”环境模式才是正确方向。全局安装包的做法是错误的。

    根本不该存在“全局”概念。理想状态下操作系统应保持不可变或近乎不可变,唯一例外可能是硬件驱动程序。

    (我知道有conda这类工具,但那不过是为修复根本性缺陷的范式而生的又一层解决方案。)

    1. > 这本是我一贯的信念,但对于语言和运行时这类事物尤为重要:在操作系统或系统上安装软件的概念必须终结。

      Python多年来一直在尝试消除这种观念;或者说,Linux发行版多年来一直在寻求Python的帮助来消除Linux系统中的这种观念。https://peps.python.org/pep-0668/是最新进展。

      1. 我认为这个原则可以概括为“系统不是工作区”。

        将系统当作工作区使用的做法,源于计算机要么体积微小且永远仅供单一用户使用,要么庞大到需要专职系统管理员维护——后者是唯一有权安装软件的人。这两种情况如今都已过时。

        1. 但“系统非工作区”的理念却助长了资源滥用的风气。现代计算机运行用户应用程序的速度反而不如三十年前的设备,其症结正在于此。这种现象在移动设备上尤为明显,但桌面系统同样深受其害。安卓系统曾因内存消耗过高且功耗效率低下而饱受诟病,直至大力推行原生编译代码与后台进程管控后才有所改善。与此同时,Electron应用却认为同时运行多个JavaScript环境无可厚非,仿佛工作内存取之不尽且性能不受影响。

          1. 或许如此,但这与当前讨论无关。Python的虚拟环境(virtualenvs)不会比系统级环境消耗更多内存。

    1. 将其宣传为Python兼容平台是个糟糕的决策,如今他们正逐步放弃这个方向。

  40. >顺带一提,第二种方法据称能提升兼容性——因为我们通过env定位bash,而bash未必位于/bin/bash路径下。这种说法是否属实,恕我不敢妄加评论。

    至少在NixOS系统中似乎很重要,我曾不得不重写几个使用/bin/bash却无法在NixOS运行的脚本中的shebang。

    1. 在macOS上若需要bash > 3.2版本也适用

  41. 初次接触Go语言时,我尝试编译代码。

    编译命令瞬间返回,我以为失败了。重试后结果相同。我暗自纳闷:怎么回事?直到执行ls命令,发现目录里赫然躺着a.out文件。Go编译器的速度之快令我震撼不已。

  42. 对于busybox这类场景,argv0参数至关重要

  43. 现在试试从Go脚本调用C++代码…

  44. > 这篇帖子起初纯属调侃,但越想越觉得这主意倒也不坏。

    感觉这简直是Go语言的非官方座右铭,而结果往往糟糕透顶。

  45. 在Python中使用uv库要安全得多也更好。至少你能获得空安全保障。当然,它无法达到光速运行,但至少能在脚本中实现可靠的类型检查,而非半吊子的后装式解决方案。

    1. Python在空安全方面比Go强在哪里?在Python中使用None会引发异常,这基本对应Go中使用nil导致的panic场景。Python同样缺乏诸如遍历运算符(?.)、合并运算符(??)等常见的空安全运算符。

      虽然可以滥用None的假值特性实现如var or “”的逻辑,但当涉及真实布尔值时这种做法就相当危险了。

  46. 所以整个机制之所以不是“真正的”shebang,反而要绕道通过shell执行,完全是因为Go运行时会被#字符卡住?

    这恰恰暴露了shebang机制本身的缺陷: 它要求shebang行必须存在且遵循特定结构——却又将包含该行的完整文件传递给解释器,导致解释器必须再次处理(并希望忽略)该行。

    我明白当一段文本被多个系统解析时,这种情况在理论上很有趣且能激发巧妙设计——但我认为最直接的解决方案是避免此类情况。

    因此Linux开发者或许应考虑新增shebang格式,在传递文件内容前直接剥离首行。

    1. 它根本不传递文件内容,传递的是文件路径。

      1. 没错,这是常见误解,连博文本身也重复了这个错误。

        唯一能“传递文件内容”的方式是通过标准输入流,但脚本可能需要正常使用stdin,因此此方案不可行。

  47.     在sh中尝试以下操作:
    
        ////////usr/local/go/bin/go 
    

    那么这样如何:我使用ruby或python,而非shell。

    不知不觉我这样操作已有25年之久。从未后悔过,也从未真正需要过shell。(好吧,这并非完全准确;我指的是shell脚本。我确实将bash作为主要shell,主要是因为其简洁性;但我并不使用shell脚本,除非遇到不支持ruby、python或perl的计算机时保留几个遗留脚本。不过这种情况如今极其罕见。)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

你也许感兴趣的: