2010-11-30 113 views
18

Je voudrais offrir aux utilisateurs de l'un de mes modules la possibilité d'étendre ses capacités en fournissant une interface pour appeler la fonction d'un utilisateur. Par exemple, je souhaite donner aux utilisateurs la possibilité d'être averti lorsqu'une instance d'une classe est créée et qu'elle a la possibilité de modifier l'instance avant qu'elle ne soit utilisée.Quelle est la meilleure façon d'implémenter un hook ou un callback en Python?

La façon dont je l'ai mis en œuvre, il est de déclarer une fonction d'usine au niveau du module qui fait l'instanciation:

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

Puis, quand je besoin d'une instance d'une classe dans mymodule, je factory(cls, arg1, arg2) plutôt que cls(arg1, arg2) .

Pour l'étendre, un programmeur écrire dans un autre module fonction comme ceci:

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

Installation du rappel ci-dessus ressemble à ceci:

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

Cela me semble assez simple à, mais je me demandais si vous, en tant que programmeur Python, vous attendez à une fonction pour enregistrer un rappel plutôt que de le faire avec une affectation, ou s'il y avait d'autres méthodes que vous attendez. Est-ce que ma solution semble réalisable, idiomatique et claire pour vous? Je cherche à le garder aussi simple que possible; Je ne pense pas que la plupart des applications auront besoin de chaîner plus d'un rappel d'utilisateur, par exemple (bien que le chaînage illimité soit "gratuit" avec le modèle ci-dessus). Je doute qu'ils devront supprimer les rappels ou spécifier les priorités ou l'ordre. Des modules tels que python-callbacks ou PyDispatcher me semblent être exagérés, surtout ceux-ci, mais s'il y a des avantages convaincants pour un programmeur travaillant avec mon module, je suis ouvert à eux.

+1

Quel est le problème avec l'utilisation d'une sous-classe simple pour étendre une classe? Pourquoi toute cette confusion appelle-t-elle les affaires? –

Répondre

7

En combinant l'idée d'Aaron d'utiliser un décorateur et l'idée de Ignacio d'une classe qui maintient une liste de callbacks ci-joint, plus un concept emprunté à C#, je suis venu avec ceci:

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 

La décoration d'une fonction avec @delegate permet de "joindre" d'autres fonctions.

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

Les fonctions peuvent être ajoutées au délégué avec += (et enlevés avec -=). Vous pouvez également décorer avec funcname.callback pour ajouter une fonction de rappel.

@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 

Cela ressemble-t-il à Pythonic?

+0

Ressemble plus à un délégué, ce qui n'est pas exactement Pythonic * en soi *, mais je ne vois aucun problème à le faire de cette façon. –

+0

Ouais, quand j'y pensais un peu plus, je pensais que c'était un peu comme les délégués C#. Peut-être devrais-je nommer la classe "délégué". – kindall

+0

En y réfléchissant encore plus, je n'ai pas vraiment besoin de la métaclasse. Au départ, je voulais être capable d'obtenir la fonctionnalité sans instancier la classe, mais comme je l'ai écrit maintenant, il serait probablement plus compréhensible avec l'instanciation. Un peu plus de travail est probablement justifié. – kindall

6

Je pourrais utiliser un décorateur afin que l'utilisateur puisse simplement écrire.

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

Ensuite, dans votre module,

import sys 

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

C'est une approche intéressante.Je pourrais ajouter un wrapper qui fait l'appel 'chain', pour que l'utilisateur n'ait pas à écrire cette partie. Il supporte également bien le cas d'utilisation où l'utilisateur veut ajouter la fonction de rappel à un autre moment que lorsque la fonction est définie. – kindall

11

Prendre aaronsterling's idea un peu plus loin:

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 
+0

C'est une approche vraiment élégante pour le cas d'utilisation spécifique de l'instanciation d'une classe. – kindall

+0

La même chose peut être faite pour les méthodes normales. –