.pth
有个饱受争议的点,就是它允许执行任意代码(参考 site 文档)
.pth 是干嘛的
.pth
文件是一类文本文件,如果放在 site-packages 目录下,python 解释器在启动阶段,会读取并解释其内容。.pth
的内容很简单,一行行的,每行只能是以下三者之一:
- 一个路径,这个路径会被添加到
sys.path
中。 有一定 python 经验的人应该都了解过sys.path
是什么。简单来说就是加载 module 的根目录列表。 - 一个 import 开头的语句,这个语句会被
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- …… 能想到的还有很多,但今天挺晚了,我先回去睡觉了