说明为什么在导入过程中运行代码是个坏主意(以及它是如何发生的)

104 阅读3分钟

这是 Python 编程中的一个公认的智慧,虽然你可以让你的模块在import'd 时运行代码,但你通常不应该这样做。导入一个模块应该是既快速又可预测的,尽可能地少做。但这一规则并不总是被遵守,当它不被遵守时,你会得到不好的结果

如果你远程登录了一台Fedora机器(并且那里没有控制台会话),并且安装了python3-keyring包,'python3 -c "import keyring"'需要25秒左右,因为该模块在导入时试图与keyrings对话,并且等待一些长时间的超时。干得不错。

(keyring模块(也)提供了 "一种访问系统钥匙圈服务的简单方法"。)

一方面,这提供了另一个关于为什么在导入时运行代码是非常糟糕的海报,因为仅仅导入一个模块显然不应该让你的 Python 程序停止 25 秒。另一方面,我认为这个案例提供了一个有趣的例子,说明了如何通过合理的 API 选择漂移到这种状态是可能的。

Keyring 有一个后端概念,它实际上与各种不同的系统钥匙圈服务对话。要使用钥匙圈,你需要选择一个要使用的后端并初始化它,"你 "指的是 "钥匙圈",因为调用钥匙圈的人只是想使用一个通用的 API,而不需要关心这个系统上使用的是什么后端。所以当你导入keyring 模块时,core.py会在导入时选择并初始化一个后端:

# init the _keyring_backend
init_backend()

在导入时自动选择和初始化一个后端意味着keyring的API已经准备好供调用者立即使用,而不需要任何进一步的工作。这是一个友好的 API,但它假定每个导入 keyring 的人都会继续使用它。虽然这听起来很合理,但一个 Python 程序可能只需要在某些情况下与钥匙圈对话,而大部分时间都不会使用它。 一个这样的程序是pip,它只是很少需要钥匙圈,但一直都在导入它。

(如果你的程序在一个函数或类中做了'import',人们就会对你嗤之以鼻,而且使用结果也会更难)。

然而,在导入时选择后端有一个缺点,至少在 Linux 上是这样的,那就是 keyring 必须弄清楚哪些系统的 keyring 服务现在实际上是活跃的,因为以 Linux 的方式,它们不止一个(keyring 支持SecretStorage和直接使用KWallet,加上第三方的插件)。由于 keyring 已经决定在导入时选择它要使用的后端,所以它必须确定在导入时,它所支持的系统中哪些 keyring 服务是活跃的。

一些 keyring 的后端通过尝试与服务建立DBus连接来确定相应的系统服务是否处于活动状态。 在正确(或错误)的情况下,这个 DBus 操作可能会拖延相当长的时间。例如,你可以在kwallet后端代码中看到这一点;它试图从org.kde.kwalletd5获取DBus对象/modules/kwalletd5。在某些情况下,这个DBus动作只有在长时间超时后才会失败,现在你有一个25秒的import延迟。

这个导入延迟并不是一个简单的情况,即keyring模块正在运行一堆重量级的代码。相反,keyring正在做一个潜在的危险操作,在导入过程中与外部服务对话。这种情况不一定很明显,因为你需要了解在特定的后端发生了什么,以及在导入时做了什么(孤立地看,每件事情听起来都很合理)。而很多时候,与外部服务的对话要么工作正常并迅速,要么立即失败。