Какой предпочтительный способ реализовать hook или callback в Python?
Я хотел бы предоставить возможность пользователям одного из моих модулей расширять свои возможности, предоставляя интерфейс для вызова пользовательской функции. Например, я хочу дать пользователям возможность получать уведомления, когда создается экземпляр класса, и предоставляется возможность изменить экземпляр до его использования.
Способ, которым я реализовал это, - объявить функцию уровня factory на уровне модуля, которая создает экземпляр:
# 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-callbacks или PyDispatcher кажутся мне излишними, особенно последними, но если есть являются неотъемлемыми преимуществами для программиста, работающего с моим модулем, я открыт для них.
Ответы
Ответ 1
Объединив идею Aaron об использовании декоратора и идеи Ignacio класса, который поддерживает список подключенных обратных вызовов, плюс концепт, взятый из С#, я придумал следующее:
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
Чувствует ли это Pythonic?
Ответ 2
Взяв aaronsterling idea немного дальше:
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
Ответ 3
Я мог бы использовать декоратор, чтобы пользователь мог просто написать.
@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