Какой предпочтительный способ реализовать хук или обратный вызов в Python? - PullRequest
20 голосов
/ 30 ноября 2010

Я хотел бы предоставить пользователям одного из моих модулей возможность расширять его возможности, предоставляя интерфейс для вызова пользовательских функций.Например, я хочу дать пользователям возможность получать уведомления при создании экземпляра класса, а также возможность изменять экземпляр перед его использованием.

Способ, который я реализовал, заключается в объявлениифункция фабрики уровня модуля, которая выполняет создание:

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

Затем, когда мне нужен экземпляр класса в mymodule, я делаю factory(cls, arg1, arg2) вместо cls(arg1, arg2).

Toрасширить его, программист мог бы написать в другом модуле функцию, подобную этой:

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 , кажутся мне излишними, особенно последние, но если у программиста, работающего с моим модулем, есть убедительные преимущества, я открыт для них.

Ответы [ 3 ]

12 голосов
/ 30 ноября 2010

Взяв идею Ааронстерлинга чуть дальше:

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
8 голосов
/ 01 декабря 2010

Объединяя идею Аарона об использовании декоратора и идею Игнасио о классе, который поддерживает список прикрепленных обратных вызовов, плюс концепцию, заимствованную из 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

Это похоже на Pythonic?

6 голосов
/ 30 ноября 2010

Я мог бы использовать декоратор, чтобы пользователь мог просто написать.

@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
...