2011-12-16 12 views
6

Lasciare spam essere un'istanza di qualche classe Spam e supporre che spam.ham sia un oggetto di un tipo predefinito, ad esempio dict. Anche se Spam non è una sottoclasse di dict, vorrei le sue istanze avere la stessa API come una normale dict (cioè gli stessi metodi con le stesse firme), ma voglio evitare di scrivere fuori un metodi bazillion boilerplate della forma:Come automatizzare la delega di __special_methods__ in Python?

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

ho provato la seguente:

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

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

... ma funziona per i metodi "normali", come keys e items, ma non per metodi speciali, come __setitem__, __getitem__, e __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() 


Tutti i metodi speciali che ho provato hanno prodotto errori simili.

Come posso automatizzare la definizione di metodi speciali (in modo da ottenere di cui il delegato)?

Precisazione: non sono necessariamente alla ricerca di soluzioni che sfruttino la sequenza di ricerca del metodo standard. Il mio obiettivo qui è di ridurre al minimo il codice di caldaia.

Grazie!

+1

Sono sicuro che hai una buona ragione, ma potresti spiegare per favore perché non vuoi rendere 'Spam' una sottoclasse di' dict'? – zwol

+0

Urgh. Secondo Zack: se vuoi apparire come un "dict", allora erediti da "dict". – katrielalex

risposta

5

Questo non può essere utile se avete bisogno di una soluzione che vieta metaclassi pure, ma qui è la soluzione mi si avvicinò con:

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() 

sembra di fare quello che stai cercando:

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

Se su Python 3.x utilizzare class Spam(object, metaclass=DictMeta) e rimuovere la linea dal corpo di Spam.

0

Non sono sicuro __getattribute__ aiuterà, ma la ragione è dei metodi speciali sono guardato nella classe non nell'istanza: http://docs.python.org/reference/datamodel.html#special-method-lookup-for-new-style-classes, come ad esempio i metodi speciali come __getattr__ e __getattribute__ stessi devono essere guardato da qualche parte.

Proxying come questo sembra in cerca di guai a me senza un'attenta riflessione, ad esempio come dovrebbe cose come __dict__ e __class__ si comportano e circa possibili conflitti metodo se l'involucro succede ad avere tutti i metodi, e che non ci sono altri problemi.

Re: is-a contro-ha un:

Se basta duplicare intera interfaccia di utente contenuta, sembra che anti-modello per me, come questo è ciò che l'eredità è per. Cosa succede se hai due ha-una relazione con due oggetti dict?

In has-a relazione, di solito si sceglie spesso metodi utili esportandoli sotto nomi diversi per rendere API sensibile. Così, invece Spam.append(item) si avrebbe Spam.addBot(bot).

1

Questo sembra un lavoro per ... un metaclasse!

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') 
        } 

Funziona!

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' 

Si noti che è necessario specificare quali parti dell'interfaccia si sta utilizzando (altrimenti, perché non solo ereditare?). Quindi abbiamo __contains__ da list e __getitem__ da dict e da __iter__ da nessuno dei due. (E solo un modo per mutare l'elenco sottostante, usando append ma non extend o __delitem__.) Quindi (come Martian) non sono sicuro di quanto utile sarà.

1

accesso attributo per tipi speciali non obbedisce le normali regole di accesso di attributo, in fondo devono esistere questi metodi a livello di classe, leggere http://docs.python.org/reference/datamodel.html#special-method-lookup-for-new-style-classes

Quindi è necessario aggiungere tutti quei metodi in modo manuale oppure è possibile aggiungere alla classe programmaticamente e il modo migliore per farlo è attraverso il metaclasse. Si noti inoltre che io non sono l'aggiunta di tutti i metodi di dict ma solo metodi speciali perché resto può essere facilmente reindirizzato attraverso __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'] 

uscita:

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

responsabilità: IMO questo è troppo magia e dovrebbe essere evitato eccetto per il divertimento :)