Python 启动钩子:.pth 文件的神奇用法✨

0 阅读5分钟

.pth 有个饱受争议的点,就是它允许执行任意代码(参考 site 文档)

.pth 是干嘛的

.pth 文件是一类文本文件,如果放在 site-packages 目录下,python 解释器在启动阶段,会读取并解释其内容。.pth的内容很简单,一行行的,每行只能是以下三者之一:

  1. 一个路径,这个路径会被添加到 sys.path 中。 有一定 python 经验的人应该都了解过 sys.path 是什么。简单来说就是加载 module 的根目录列表。
  2. 一个 import 开头的语句,这个语句会被 exec() 执行。 本质上其实允许了执行任意代码
  3. 一个 # 开头的行,就是注释,跳过。

exec

其中这个第二点就是比较危险,比如有害的第三方库如果骗你安装了,就可以在你每次正常启动 python 时,在执行你常规代码之前,进行一些 monkey patching 的操作

当然你安装一个包当然就是信任它了,所以怪不得别人 😂 只是想提醒大家,你安装一个包之后即使不用命令行或者 import 的方式调用它,它也可能可以让你在不经意间执行它。比起更广为人知的通过 sdist 在 build 阶段执行人代码的安全问题,这个方式更鲜为人知。


但是这篇文章不是想讲这些的,而是想讲讲我发现了这个功能可以有一些很好的用法

好的用途

加载环境变量很讨厌。要么你安装 python-dotenv 然后用 dotenv run 启动你的项目,要么在某个最早调用的模块里调用 dotenv.load_dotenv(override=True),如果你只想在开发阶段加载 .env(因为生产环境一般直接在平台上设置环境变量,不通过 .env 文件),可能还得放在一个 try-expect 块中,甚至写上这样的模板代码:

from contextlib import suppress

with suppress(ImportError):
    from dotenv import load_dotenv

    load_dotenv(override=True)

多丑陋!而且这样会多出一个令人疑惑的 __init__.py(因为你往往希望尽可能早地进行这个调用)。在我写 hmr 之前,还涉及到一个 overhead 的问题:contextlib 不是一个很轻的标准库。

今天我写了一个超小的库:CNSeniorious000/dotenv.pth: load .env when python starts - GitHub

当你安装了这个库,每次你任何方式启动 python 或者用什么 cli 工具,它都会自动 load 环境变量。这意味着如果你只想在开发环境中加载 .env,你只需要把 dotenv.pth 装在 dev dependencies 中就行了。


另一个例子是,我的 hmr-daemon,它现在还没有任何文档,但我可以在这里简单说一下:

  • 它启动一个 daemon 线程,注册 hmr 中的 ModuleFinder,使得所有当前项目的模块都是 ReactiveModule
  • 但是所有 reload 都在这个线程中执行,所以你看到的效果就是,如果你的代码还在运行,这时候你保存其中某个文件(比如更新一个函数的实现,或者比如更改某个参数),立即就能看到效果

常用的场景是,你想用 python 的 shell(或者 ipython 之类的)来交互式地调试你的项目,但是当你修改项目文件后,你肯定懒得关闭再开启 shell 一遍。

通过 hmr-daemon,你不用重启 shell,所有 from module import value 得到的 value 都是最新的。或者 module.value 这样的也是最新的。

一个坑

开发过程中往往会在虚拟环境中开发,但是将虚拟环境的 site-packages 添加到 sys.path 中的逻辑本身也是通过一个 .pth 文件实现的,它叫 _virtualenv.pth,而 python 执行 .pth 是按字典序执行的,所以如果你的 .pth 文件以 _ 开头,就很可能没法 import 一个在虚拟环境中安装了的包,因为这时候虚拟环境可以说还没别“激活”—— 激活虚拟环境的 .pth 还没执行呢。

debug 终于发现是这个原有的时候还挺搞笑的 😭

总结一下

我认为 .pth 是个很 Pythonic 的特性,很鲜明地展现了 Python 的个性 —— 极高的自定义性。可以用它实现很多提高 DX(developer experience)的小功能。

而且我认为这个东西并没有被人开发得很完全,我想我可能未来也会开发更多基于 .pth 的有意思的库。

比如我现在想到的几个是:

  • 在 Windows 上 Python 开发有个很坑人的点,它文件的默认编码是 GBK 而不是 utf-8,所以到处需要指定。我们可以在 .pth 中 monkey patch 这个
  • 可以监测 .env 的变化,然后随时更新到当前进程的环境变量(不过这个 hmr 本身也快要能实现了:它现在的依赖图不止支持 python 模块也支持任意文件了,正在 reactive-fs 分支积极开发中,说起来这个功能可能最早受启发于这条评论
  • nested-asyncio 这样的“只暴露一个无参函数”的包,完全可以发布一个 .pth 包,这样不用在代码中显示地 patch,而是在依赖中这么做
  • 用来设置。比如用 pip install package[extra1,extra2],而这两个 extra 分别只是一个 .pth 文件,import os 并设置一个环境变量而已,这样实现 config-as-code
  • …… 能想到的还有很多,但今天挺晚了,我先回去睡觉了