Python:お手軽にPluggableにする

Pythonで自分用の小物アプリを結構書き溜めてるんですけど、実はそのほとんどにプラグインシステムみたいなのをつくってたりします。

たとえばファイルの整理自動化みたいなの。ファイルの移動前に処理を追加する、てな部分をプラグインにしてあるわけです。

まぁ機能的にはCPANのClass::Componentっぽいもんですね。ですがそこまで高機能なのはいらないので、シンプルに自分が必要な部分だけまとめてみました。

 1from inspect import getmembers, ismethod
 2import types
 3
 4class Pluggable(object):
 5  def __init__(self):
 6    self.hooks = {}
 7
 8  def load(self, *config):
 9    self.load_config(*config)
10    self.load_plugins()
11
12  def load_config(self, *config):
13    self.config = []
14    for i in config:
15      if isinstance(i, ("".__class__, u"".__class__)):
16        self.config.append((i, {}))
17      else:
18        self.config.append((i[0], len(i) > 1 and i[1] or {}))
19
20  def load_plugins(self):
21    for klass,config in self.config:
22      plugin = None
23      try:
24        plugin = eval(klass)
25      except (NameError, AttributeError),e:
26        names = klass.split(".")
27        mod = __import__(".".join(names[0:-1]))
28        globals()[names[0]] = mod
29        for name in names[1:-1]: mod = getattr(mod, name)
30        plugin = getattr(mod, names[-1])
31
32      p = plugin(config)
33      make_closure = lambda obj,name: lambda *a, **k: getattr(obj,name)(*a, **k)
34
35      for name, method in ((n,m) for n,m in getmembers(p) if ismethod(m)):
36        if hasattr(method, "__plugin_method__"):
37          setattr(self, name, make_closure(p, name))
38        for hook_type in getattr(method, "__hook_types__", []):
39          if hook_type not in self.hooks: self.hooks[hook_type] = []
40          self.hooks[hook_type].append(getattr(p, name))
41
42  def _run_hooks(self, dct, f=lambda x:x):
43    result = {}
44    for (name,value) in dct.iteritems():
45      if name not in self.hooks: continue
46      k,a = {}, value
47      if isinstance(value[-1], types.DictType):
48        k,a = value[-1],value[0:-1]
49      result[name] = [m(*a, **k) for m in f(self.hooks[name])]
50    return result
51
52  def run_hooks(self, dct):
53    return self._run_hooks(dct)
54
55  def reverse_run_hooks(self, dct):
56    return self._run_hooks(dct, reversed)
57
58class Plugin(object):
59  def __init__(self, config):
60    self.config = config
61
62method = lambda f: setattr(f, "__plugin_method__", True) or f
63hook = lambda *args: lambda f: setattr(f, "__hook_types__", args) or f

機能的には

  • クラスに対して設定とともにメソッドを追加する(setattrします)
  • 任意のhookの登録と実行

ぐらいです。

使い方は

 1import pluggable
 2class People(pluggable.Pluggable):
 3  pass
 4
 5class Hacker(pluggable.Plugin):
 6  @pluggable.method
 7  def hack(self):
 8    print self.config["msg"]
 9
10  @pluggable.hook("hello")
11  def hello(self, msg1, msg2, msg3):
12    print "Hello World!!", msg1, msg2, msg3
13
14class Otaku(pluggable.Plugin):
15  @pluggable.method
16  def niconico(self, msg):
17    print self.config["msg"], msg
18
19  @pluggable.hook("hello")
20  def hello(self, msg1, msg2, msg3):
21    print u"にぱー", msg1, msg2, msg3

みたいにプラグインクラスを作っておいて

 1p = People()
 2p.load(
 3  ("plugins.Hacker", {"msg": "Happy Hacking!"}),
 4  ("plugins.Otaku",  {"msg": u"ニコ厨ですが"})
 5)
 6p.hack() #=> Happy Hacking!
 7p.niconico(u"なにか?") #=> ニコ厨ですが なにか?
 8p.run_hooks({"hello":([1,2],{"msg3":3})})
 9#=> Hello World!! 1 2 3
10#=> にぱー 1 2 3
11p.reverse_run_hooks({"hello":[1,2,3]})
12#=> にぱー 1 2 3
13#=> Hello World!! 1 2 3

という感じで使います。あとは設定の部分をyamlなんかに押し出せばオッケーというわけ。

モジュールのインポートは自動でやってくれます。Pythonは地味に動的なimportが面倒だったので、ライブラリにまとめたことでだいぶ手元の小物アプリがすっきりしました。  


  • 2008/01/27 若干修正を加えました。
comments powered by Disqus