Python中实现钩子或回调的首选方式是什么?

26

我希望为我的一个模块的用户提供一种扩展功能的能力,即通过提供调用用户函数的接口来扩展其功能。例如,我希望给用户提供在创建类的实例并在使用之前修改实例的机会的能力。

我实现的方式是声明一个模块级别的工厂函数来进行实例化:

# in mymodule.py
def factory(cls, *args, **kwargs):
    return cls(*args, **kwargs)

那么当我需要在mymodule中实例化一个类的时候,我会使用factory(cls, arg1, arg2)而不是cls(arg1, arg2)

为了扩展它,程序员可以在另一个模块中编写以下函数:

def myFactory(cls, *args, **kwargs):
    instance = myFactory.chain(cls, *args, **kwargs)
    # do something with the instance here if desired
    return instance

安装上述回调函数的步骤如下:

myFactory.chain, mymodule.factory = mymodule.factory, myFactory

对我来说,这似乎很简单,但作为一名Python程序员,您是否希望一个函数注册回调而不是使用赋值来实现,或者您是否期望其他方法。我的解决方案对您来说是否可行、惯用且清晰?

我希望尽可能保持简单;例如,我认为大多数应用程序实际上不需要链接多个用户回调(虽然使用上述模式可以“免费”地进行无限链接)。我怀疑他们是否需要删除回调或指定优先级或顺序。像python-callbacksPyDispatcher这样的模块对我来说似乎过于复杂,特别是后者,但是如果对程序员使用我的模块有强大的好处,我会接受它们。


2
使用简单的子类来扩展一个类有什么问题吗?为什么要搞这些混乱的回调业务呢? - S.Lott
3个回答

14

aaronsterling的想法推广一下:

class C(object):
  _oncreate = []

  def __new__(cls):
    return reduce(lambda x, y: y(x), cls._oncreate, super(C, cls).__new__(cls))

  @classmethod
  def oncreate(cls, func):
    cls._oncreate.append(func)

c = C()
print hasattr(c, 'spew')

@C.oncreate
def spew(obj):
  obj.spew = 42
  return obj

c = C()
print c.spew

这是一个非常优雅的方法,适用于实例化类的特定用例。 - kindall

14

结合了Aaron使用装饰器的想法和Ignacio维护附加回调列表的类的想法,再加上从C#借鉴而来的概念,我得出了以下代码:

class delegate(object):

    def __init__(self, func):
        self.callbacks = []
        self.basefunc = func

    def __iadd__(self, func):
        if callable(func):
            self.__isub__(func)
            self.callbacks.append(func)
        return self

    def callback(self, func):
        if callable(func):
            self.__isub__(func)
            self.callbacks.append(func)
        return func

    def __isub__(self, func):
        try:
            self.callbacks.remove(func)
        except ValueError:
            pass
        return self

    def __call__(self, *args, **kwargs):
        result = self.basefunc(*args, **kwargs)
        for func in self.callbacks:
            newresult = func(result)
            result = result if newresult is None else newresult
        return result

使用@delegate修饰一个函数可以让其他函数“附加”到它上面。

@delegate
def intfactory(num):
    return int(num)

使用 += 可以将函数添加到委托中(使用 -= 可以将其移除)。还可以使用 funcname.callback 进行修饰,以添加回调函数。

@intfactory.callback
def notify(num):
    print "notify:", num

def increment(num):
    return num+1

intfactory += increment
intfactory += lambda num: num * 2

print intfactory(3)   # outputs 8

这个是否感觉像Python的编程风格?


感觉更像是委托,这并不完全符合Pythonic的风格,但我认为用这种方式做没有问题。 - Ignacio Vazquez-Abrams
是的,当我再考虑这个问题时,我认为这很像C#代理。也许我应该将这个类实际命名为“delegate”。 - kindall
再仔细想想,我其实不需要元类。我最初想要的是在不实例化类的情况下获得功能,但现在我写的方式可能更容易理解实例化。可能需要做更多的工作。 - kindall
我已经进行了上述更改。 - kindall

6
我可能会使用装饰器,这样用户只需编写。
@new_factory
def myFactory(cls, *args, **kwargs):
    instance = myFactory.chain(cls, *args, **kwargs)
    # do something with the instance here if desired
    return instance

然后在您的模块中,

import sys

def new_factory(f):
    mod = sys.modules[__name__]
    f.chain = mod.factory
    mod.factory = f
    return f

这是一个有趣的方法。我可以添加一个包装器来执行chain调用,这样用户就不必编写那部分了。它还很好地支持了用户在函数定义时以外的某个时间添加回调函数的用例。 - kindall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接