2011-12-16 9 views
6

Sea spam una instancia de alguna clase Spam, y suponga que spam.ham es un objeto de algún tipo incorporado, digamos dict. Aunque Spam no es una subclase de dict, me gustaría que sus instancias tuvieran la misma API que un dict normal (es decir, los mismos métodos con las mismas firmas), pero quiero evitar escribir un número repetitivo de los métodos del formulario:¿Cómo automatizar la delegación de __special_methods__ en Python?

def apimethod(self, this, that): 
     return self.ham.apimethod(this, that) 

he intentado lo siguiente:

class Spam(object): 
    def __init__(self): 
     self.ham = dict() 

    def __getattr__(self, attr): 
     return getattr(self.ham, attr) 

... pero funciona para métodos "normales", como keys y items, pero no para métodos especiales, como __setitem__, __getitem__, y __len__:

>>> spam = Spam() 
>>> spam.keys() 
[] 
>>> spam['eggs'] = 42 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
TypeError: 'Spam' object does not support item assignment 
>>> spam.ham['eggs'] = 42 
>>> foo.items() 
[('eggs', 42)] 
>>> spam['eggs'] 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
TypeError: 'Spam' object is not subscritable 
>>> len(spam) 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
TypeError: 'Spam' object has no len() 


Todos los métodos especiales que probé producen errores similares.

Cómo puedo automatizar la definición de métodos especiales (de modo que consigan que se refiere el delegado)?

Aclaración: No necesariamente estoy buscando soluciones que aprovechen la secuencia de búsqueda de métodos estándar. Mi objetivo aquí es minimizar el código repetitivo.

Gracias!

+1

Estoy seguro de que tienes una buena razón, pero ¿podrías explicar por qué no quieres que 'Spam' sea una subclase de' dict'? – zwol

+0

Urgh. Segundo Zack: si quieres parecer un 'dict' entonces hereda de' dict'. – katrielalex

Respuesta

5

Esto puede no ser útil si necesita una solución que prohíbe metaclases así, pero aquí es la solución que se me ocurrió:

def _wrapper(func): 
    def _wrapped(self, *args, **kwargs): 
     return getattr(self.ham, func)(*args, **kwargs) 
    return _wrapped 

class DictMeta(type): 
    def __new__(cls, name, bases, dct): 
     default_attrs = dir(object) 
     for attr in dir(dict): 
      if attr not in default_attrs: 
       dct[attr] = _wrapper(attr) 
     return type.__new__(cls, name, bases, dct) 

class Spam(object): 
    __metaclass__ = DictMeta 
    def __init__(self): 
     self.ham = dict() 

parece hacer lo que estás buscando:

>>> spam = Spam() 
>>> spam['eggs'] = 42 
>>> spam.items() 
[('eggs', 42)] 
>>> len(spam) 
1 
>>> spam.ham 
{'eggs': 42} 

Si en Python 3.x usa class Spam(object, metaclass=DictMeta) y elimina la línea __metaclass__ del cuerpo de Spam.

0

__getattribute__ No estoy seguro va a ayudar, pero la razón es los métodos especiales que se buscan en la clase no en la instancia: http://docs.python.org/reference/datamodel.html#special-method-lookup-for-new-style-classes, como por ejemplo los métodos especiales como __getattr__ y __getattribute__ mismas tienen que ser consultados en alguna parte.

Proxy como esto parece estar pidiendo problemas a mí sin una reflexión cuidadosa, por ejemplo, ¿cómo debería cosas como __dict__ y __class__ se comportan y sobre posibles conflictos de método si su envoltura pasa a tener cualquiera de los métodos, y seguro que hay otros problemas.

Re: is-a-frente tiene una:

Si acaba de duplicar la interfaz conjunto de miembros que figura, parece que anti-patrón para mí, ya que eso es lo que la herencia es para. ¿Qué pasa si tienes dos relaciones con dos objetos dict?

En tiene una relación, uno generalmente elige métodos útiles a menudo exportándolos bajo diferentes nombres para hacer API sensible. Entonces, en cambio, Spam.append(item) tendrías Spam.addBot(bot).

1

Esto parece un trabajo para ... ¡una metaclase!

def make_method(p, m): 
    def method(self, *a, **k): 
     return getattr(getattr(self, p),m)(*a, **k) 
    return method 

class Proxier(type): 
    def __new__(cls, name, bases, dict): 
     objs = dict.get('proxyobjs', []) 
     if objs: 
      old_init = dict.get('__init__', lambda self: None) 
      def new_init(self, *a, **k): 
       for (n,v) in objs.iteritems(): 
        setattr(self, n, v()) 
       old_init(self, *a, **k) 
      dict['__init__'] = new_init 
      meths = dict.get('proxymethods', {}) 
      for (proxyname, methnames) in meths.iteritems(): 
       for methname in methnames:     
        dict[methname] = make_method(proxyname, methname) 
     return super(Proxier, cls).__new__(cls, name, bases, dict) 


class Spam(object): 
    __metaclass__ = Proxier 
    proxyobjs = {'ham': dict, 
       'eggs': list, 
       } 
    proxymethods = {'ham': ('__setitem__', '__getitem__', '__delitem__'), 
        'eggs': ('__contains__', 'append') 
        } 

¡Funciona!

In [28]: s = Spam() 

In [29]: s[4] = 'hi' 

In [30]: s.append(3) 

In [31]: 3 in s 
Out[31]: True 

In [32]: 4 in s 
Out[32]: False 

In [33]: s[4] 
Out[33]: 'hi' 

Tenga en cuenta que debe especificar qué partes de la interfaz está utilizando (de lo contrario, ¿por qué no heredar?). Así que tenemos __contains__ desde list y __getitem__ desde dict, y __iter__ desde ninguna de las dos. (Y solo una forma de cambiar la lista subyacente, usando append pero no extend o __delitem__). Entonces (como marciano) no estoy seguro de lo útil que será esto.

1

acceso Atributo de métodos especiales no obedece las reglas de acceso de atributos normales, básicamente, tiene que existir esos métodos a nivel de clase, leer http://docs.python.org/reference/datamodel.html#special-method-lookup-for-new-style-classes

Así que hay que añadir todos esos métodos, ya sea manualmente o se puede añadir a la clase programáticamente y la mejor manera de hacerlo es a través de metaclass. También tenga en cuenta que no estoy añadiendo todos los métodos en dict pero sólo los métodos especiales porque el descanso puede ser fácilmente redirigido a través __getattr__

def redirect(methodname): 
    def _redirect(self, *args, **kwargs): 
     print "redirecting",methodname 
     method = getattr(self.ham, methodname) 
     return method(*args, **kwargs) 

    return _redirect 

class DictRedirect(object): 
    def __new__(cls, name, bases, attrs): 

     # re-create all special methods from dict 
     dict_attr_names = set(dir(dict)) 
     common_names = set(dir(cls)) 
     for methodname in dict_attr_names-common_names: 
      if not methodname.startswith('__'): 
       continue 
      attrs[methodname] = redirect(methodname) 
     return type(name, bases, attrs) 

class Spam(object): 
    __metaclass__ = DictRedirect 

    def __init__(self): 
     self.ham = dict() 

    def __getattr__(self, name): 
     return getattr(self.ham, name) 

spam = Spam() 
spam['eggs'] = 'yolk' 
print 'keys =',spam.keys() 
print spam['eggs'] 

de salida:

redirecting __setitem__ 
keys = ['eggs'] 
redirecting __getitem__ 
yolk 

responsabilidad: OMI esto es demasiado magia y debe ser evitado, excepto por la diversión :)

Cuestiones relacionadas