2012-07-07 21 views
7

Estoy intentando escribir un decorador que proporcione funcionalidad de sobrecarga de método a python, similar al mencionado en PEP 3124.Decodificador de sobrecarga de método

El decorador que escribí funciona muy bien para las funciones normales, pero no puedo hacer que funcione para los métodos en una clase.

Aquí es el decorador:

class Overload(object): 
    def __init__(self, default): 
     self.default_function = default 
     self.type_map = {} 
     self.pos = None 

    def __call__(self, *args, **kwargs): 
     print self 
     try: 
      if self.pos is None: 
       pos = kwargs.get("pos", 0) 
      else: 
       pos = self.pos 
      print args, kwargs 
      return self.type_map[type(args[pos])](*args, **kwargs) 
     except KeyError: 
      return self.default_function(*args, **kwargs) 
     except IndexError: 
      return self.default_function(*args, **kwargs) 

    def overload(self, *d_type): 
     def wrapper(f): 
      for dt in d_type: 
       self.type_map[dt] = f 
      return self 
     return wrapper 

Cuando intento poner en práctica esta manera:

class MyClass(object): 
    def __init__(self): 
     self.some_instance_var = 1 

    @Overload 
    def print_first_item(self, x): 
     return x[0], self.some_instance_var 

    @print_first_item.overload(str) 
    def print_first_item(self, x): 
     return x.split()[0], self.some_instance_var 

consigo una cuando lo ejecuto TypeError:

>>> m = MyClass() 
>>> m.print_first_item(1) 
<__main__.Overload object at 0x2> (1,) {} 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
    File "overload.py", line 17, in __call__ 
    return self.default_function(*args, **kwargs) 
    TypeError: print_first_item() takes exactly 2 arguments (1 given) 
>>> 

Mi pregunta es: ¿Cómo puedo acceder a la instancia de MyClass (es decir, self) desde dentro del método decorado?

+2

¿Has mirado en la implementación de referencia en PEAK.Rules, o cualquiera de las docenas de otras implementaciones de referencia vinculados a la Pje PEP anteriores y publicaciones en la lista? Si intentas usar esto, en lugar de tratar de explorar Python, probablemente tenga más sentido usar su trabajo (que al menos algunas otras personas hayan usado y probado) que repetirlo. – abarnert

+0

@abarnert: No estaba al tanto de esto. Gracias por el aviso. Habiendo dicho eso, realmente me pregunto por qué mi implementación no está funcionando como esperaba y cómo podría solucionarlo. Yo soy, como dices "explorando Python". –

+3

Primero, ¿sabes de @ functools.wraps, etc.? Harán tu vida mucho más fácil, pero eso no te ayudará aquí. De todos modos, la primera parte del problema aquí es que su default_function está siendo reemplazada por una clase funcional que no es un método (Overload .__ call__ toma un self, pero esa es la instancia de Overload, no la MyClass). Pero obviamente no puedes simplemente hacer __call__ (self, realself, * args, ** kwargs) y esperar que eso funcione. No tengo tiempo para entrar en detalles esta noche; con suerte, alguien más puede ayudar antes de que regrese. – abarnert

Respuesta

1

Esencialmente, su clase Overload necesita un método __get__:

def __get__(self, obj, cls): 
    # Called on access of MyClass.print_first_item. 
    # We return a wrapper which calls our 
    print "get", self, obj, cls 
    if obj is None: 
     # a function would do some checks here, but we leave that. 
     return self 
    else: 
     return lambda *a, **k: self(obj, *a, **k) 

¿Por qué?

Bueno, utiliza su objeto Overload como una especie de reemplazo de función. Lo quiere, como una función, para representarse en un contexto de método con una firma diferente.

Breve explicación de cómo funciona el acceso método:

object.meth(1, 2) 

se traduce a

object.__dict__['meth'].__get__(object, type(object))(1, 2) 

de __get__() Una función devuelve un objeto método que envuelve la función anteponiendo el objeto a la lista de parámetros (donde resultados en self):

realmethod = object.__dict__['meth'].__get__(object, type(object)) 
realmethod(1, 2) 

donde realmethod es un objeto método que conoce la función a ser llamada y la self que debe darse a la misma y llama a la función "real" apropiadamente mediante la transformación de la llamada en

meth(object, 1, 2) 

.

Este comportamiento imitamos en este nuevo método __get__.

+0

Impresionante. Utilizando tu consejo, lo hice funcionar. +1 para la explicación detallada de cómo funcionan las búsquedas. –

+1

Ver también mi implementación de trabajo a continuación. –

1

como dice abarnert, ya que está utilizando una clase ya que su decorador 'self' es una instancia de Overload en lugar de MyClass como espera y espera.

No he podido encontrar una solución simple. Lo mejor que se me ocurre es no usar una clase como decorador y utilizar una función en vez de un segundo argumento con un diccionario predeterminado. Como se trata de un tipo mutable, será el mismo diccionario cada vez que se llame a la función. Yo uso esto para almacenar mis 'variables de clase'. Los restos siguen un patrón similar a su solución.

Ejemplo:

import inspect 

def overload(funcOrType, map={}, type=None): 
    if not inspect.isclass(funcOrType): 
     # We have a function so we are dealing with "@overload" 
     if(type): 
      map[type] = funcOrType 
     else: 
      map['default_function'] = funcOrType 
    else: 
     def overloadWithType(func): 
      return overload(func, map, funcOrType) 
     return overloadWithType 

    def doOverload(*args, **kwargs): 
     for type in [t for t in map.keys() if t != 'default_function'] : 
      if isinstance(args[1], type): # Note args[0] is 'self' i.e. MyClass instance. 
       return map[type](*args, **kwargs) 
     return map['default_function'](*args, **kwargs) 

    return doOverload 

continuación:

from overload import * 

class MyClass(object): 
    def __init__(self): 
     self.some_instance_var = 1 

    @overload 
    def print_first_item(self, x): 
     return x[0], self.some_instance_var 

    @overload(str) 
    def print_first_item(self, x): 
     return x.split()[0], self.some_instance_var 


m = MyClass() 
print (m.print_first_item(['a','b','c'])) 
print (m.print_first_item("One Two Three")) 

yeilds:

('a', 1) 
('One', 1) 
1

Para referencia, aquí es la implementación de trabajo, gracias a la explicación detallada por glglgl:

argtype_tuple = lambda args: tuple(type(a) for a in args) 

class Overload(object):  
    def __init__(self, func): 
     self.default = func 
     self.map = {} 

    def __call__(self, *args, **kwargs): 
     key_tuple = argtype_tuple(args) 
     c_inst = kwargs.pop("c_inst", None) 
     if c_inst: 
      args = (c_inst,) + args 
     try: 
      return self.map[key_tuple](*args, **kwargs) 
     except KeyError: 
      return self.default(*args, **kwargs) 

    def __get__(self, obj, cls): 
     if obj: 
      return lambda *args, **kwargs: self(c_inst=obj, *args, **kwargs) 
     else: 
      return self 

    def overload(self, *types): 
     def wrapper(f): 
      for type_seq in types: 
       if type(type_seq) == tuple: 
        type_seq = tuple(type_seq) 
       else: 
        type_seq = (type_seq,) 
       self.map[type_seq] = f 
      return self 
     return wrapper 

#Some tests/usage examples 
class A(object): 
    @Overload 
    def print_first(self, x): 
     return x[0] 

    @print_first.overload(str) 
    def p_first(self, x): 
     return x.split()[0] 

    def __repr__(self): 
     return "class A Instance" 

a = A() 
assert a.print_first([1,2,3]) == 1 
assert a.print_first("one two three") == "one" 

@Overload 
def flatten(seq): 
    return [seq] 

@flatten.overload(list, tuple) 
def flat(seq): 
    return sum((flatten(item) for item in seq), []) 

assert flatten([1,2,[3,4]]) == [1,2,3,4] 
assert flat([1,2,[3,4]]) == [1,2,3,4]