让 Python 模块可调用:介绍 Cadule
Source: Dev.to

在编写 Python 代码时,我常常想念 Node.js 的一个特性:能够直接 require 一个模块并让它可调用。在 Node.js 中,你可以从模块导出一个函数并立即调用它:
const messUpThings = require('./mess-up-things');
messUpThings(); // Works!
我知道有些开发者不喜欢这种模式,但它确实有合理的使用场景。
问题
设想一种情况,你需要编写一个自解释名称的帮助函数,例如 mess_up_things。函数名本身就说明了它的作用。你有几种选择:
- 创建一个 util 或 helper 模块:把它放在
util.py或helper.py中。许多开发者不喜欢这些通用名称,尤其是更倾向于具描述性包名的 Go 开发者。 - 创建一个专用文件:把它放在
mess_up_things.py中。然后你需要写冗长的导入语句,例如:
from mess_up_things import mess_up_things
这种重复显得多余。如果能够直接这样做就好了:
import mess_up_things
mess_up_things()
探索解决方案
如果你熟悉 Python 的魔法方法,你可能知道任何拥有 __call__ 方法的对象都可以被调用。由于模块是 types.ModuleType 的实例,并且它们支持像 __getattr__ 这样的魔法方法,难道我们不能直接在模块上定义 __call__ 方法吗?
遗憾的是,Python 并不原生支持这一点。实际上曾有 PEP 713 提议为模块层面添加 __call__ 支持,但该提案被否决了。
不过,有一种技巧可以实现这种行为:
import sys
class MyModule(sys.modules[__name__].__class__):
def __call__(self):
print("Messing up things...")
sys.modules[__name__].__class__ = MyModule
它通过在运行时动态替换模块的 __class__ 来工作。但谁愿意每次都复制粘贴这段样板代码呢?
介绍 Cadule
为了避免重复这段样板代码,我把机制封装进了一个名为 Cadule(Callable [Mo]dule Less 的缩写)的包中。
安装
pip install cadule
使用
创建一个名为 mess_up_things.py 的文件:
import cadule
@cadule
def __call__():
print("Messing up things...")
然后在 Python REPL 或其他脚本中:
>>> import mess_up_things
>>> mess_up_things()
Messing up things...
>>> callable(mess_up_things)
True
你也可以传递参数并返回值:
import cadule
@cadule
def __call__(target):
return f"Messing up {target}!"
>>> import mess_up_things
>>> mess_up_things("the database")
'Messing up the database!'
何时使用
Cadule 在以下场景特别有用:
- 单一用途模块:当模块的主要目的是暴露一个函数时。
- DSL 与流式接口:创建更自然的 API。
- 脚本与工具:让命令行工具的使用更直观。
GitHub: