2010-11-30 10 views
18

Me gustaría proporcionar la capacidad para que los usuarios de uno de mis módulos amplíen sus capacidades al proporcionar una interfaz para llamar a la función de un usuario. Por ejemplo, quiero dar a los usuarios la capacidad de recibir notificaciones cuando se crea una instancia de una clase y se les da la oportunidad de modificar la instancia antes de que se use.¿Cuál es la forma preferida de implementar un enlace o devolución de llamada en Python?

La forma en vez implementado es declarar una función de fábrica de nivel de módulo que hace la creación de instancias:

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

Entonces cuando necesito una instancia de una clase en mimodulo, hago factory(cls, arg1, arg2) en lugar de cls(arg1, arg2) .

extenderlo, un programador podría escribir en otro módulo de una función como esta:

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

Instalación de la devolución de llamada citadas es el siguiente:

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

Esto parece suficientemente claro para mí, pero me preguntaba si usted, como programador de Python, esperaría que una función registre una devolución de llamada en lugar de hacerlo con una tarea, o si hubiera otros métodos que podría esperar. ¿Mi solución parece funcional, idiomática y clara para ti?

Estoy buscando que sea lo más simple posible; No creo que la mayoría de las aplicaciones deban encadenar más de una devolución de llamada de un usuario, por ejemplo (aunque el encadenamiento ilimitado es "gratis" con el patrón anterior). Dudo que necesiten eliminar devoluciones de llamada o especificar prioridades u orden. Los módulos como python-callbacks o PyDispatcher me parecen exagerados, especialmente este último, pero si hay beneficios convincentes para un programador que trabaja con mi módulo, estoy abierto a ellos.

+1

¿Qué hay de malo en usar una subclase simple para extender una clase? ¿Por qué toda esta confusión llamada call back? –

Respuesta

7

Combinando la idea de Aaron de la utilización de un decorador y la idea de Ignacio de una clase que mantiene una lista de Devolución de llamada adjunta, más un concepto prestado de C#, se me ocurrió esto:

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 

Decorar una función con @delegate permite que se "conecten" otras funciones.

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

Las funciones pueden ser añadidos a la delegado con += (y eliminarse con -=). También puede decorar con funcname.callback para agregar una función de devolución de llamada.

@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 

¿Esto se siente Pythonic?

+0

Se siente más como un delegado, que no es exactamente pitónico * per se *, pero no veo ningún problema en hacerlo de esta manera. –

+0

Sí, cuando estaba pensando en esto un poco más, pensé que se parecía mucho a los delegados de C#. Quizás debería nombrar a la clase "delegado". – kindall

+0

Pensando en ello aún más, realmente no necesito la metaclase. Originalmente quería poder obtener la funcionalidad sin crear una instancia de la clase, pero tal como lo escribí ahora, probablemente sería más comprensible con la creación de instancias. Un poco más de trabajo probablemente esté justificado. – kindall

6

Podría usar un decorador para que el usuario pueda escribir.

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

Luego, en su módulo,

import sys 

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

Ese es un enfoque interesante.Podría agregar un contenedor que haga la llamada 'chain', para que el usuario no tenga que escribir esa parte. También admite muy bien el caso de uso en el que el usuario desea agregar la función de devolución de llamada en algún momento que no sea cuando se define la función. – kindall

11

Tomando aaronsterling's idea un poco más lejos:

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

Ese es un enfoque realmente elegante para el caso de uso específico de instanciar una clase. – kindall

+0

Lo mismo se puede hacer para los métodos normales también. –

Cuestiones relacionadas