Pythonを書いているとき、Node.jsのあの機能が懐かしいと思ったことはありませんか? そう、モジュールをrequireしたら、すぐに呼び出せるようになるあれです (´▽`)
Node.jsだとこんな感じで書けます:
const messUpThings = require('./mess-up-things');
messUpThings(); // 動くじゃん!
「このパターン嫌いだよ〜」という開発者の方もいると思いますが、実は便利な場面もあるんですよね (´・ω・`)
問題点
例えば、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の魔法メソッド(magic methods)に詳しい方なら、「オブジェクトに__call__メソッドがあれば呼び出し可能になる」と知っているはず。モジュールもtypes.ModuleTypeのインスタンスだし、__getattr__みたいな魔法メソッドもサポートしてるから、モジュールに直接__call__を定義すれば...?
残念ながら、Pythonはデフォルトではこれをサポートしていないんです (;´Д`
実はPEP 713という提案があって、モジュールレベルの__call__サポートを追加しようとしたのですが...採用されませんでした orz
でも! Pythonのランタイムに詳しい方なら、裏技があることに気づくかもしれません (´∀`)
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
これだけ! mess_up_thingsモジュール全体が呼び出し可能なオブジェクトになりました (*´▽*) モジュールを呼び出すと、デコレートされたcall`関数が実行されます。
引数を渡したり、戻り値を受け取ることもできます:
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は特にこんな時に便利です:
- 単一目的のモジュール: モジュールの主目的が1つの関数を公開することである場合
- DSLや流れるようなインターフェース: より自然なAPIを作りたい時
- スクリプトやユーティリティ: コマンドラインツールをより直感的に
注意点
この方法はランタイムでのクラス入れ替えに依存しているので、強力ですが考慮すべき点もあります:
- Pythonの内部
sys.modulesメカニズムを使用しています - 動的なクラス代入のため、デバッグが少し複雑になるかもしれません
- 一部の静的解析ツールはこのパターンを理解できないかも (´・ω・`)
まとめ
Pythonの公式デザイン哲学は呼び出し可能なモジュールを受け入れていませんが、Caduleは少しのランタイム魔法で、このパターンをエレガントに実現できることを証明しています。Node.jsのようにモジュールを呼び出し可能にしたい時、Caduleがきっと役に立つはずです ✨
次にfrom module import functionと書きたくなったら、Caduleを使ってもっとクールで表現力豊かなコードを書いてみませんか? (´∀`)b
GitHub: cadule
PyPI: pip install cadule