2010-06-25 15 views
21

Voglio implementare lo decorator pattern in Python e mi chiedevo se c'è un modo per scrivere un decoratore che implementa semplicemente la funzione che desidera modificare, senza scrivere il boiler-plate per tutte le funzioni che sono appena inoltrati all'oggetto decorato. In questo modo:Implementazione del pattern decorator in Python

class foo(object): 
    def f1(self): 
     print "original f1" 
    def f2(self): 
     print "original f2" 

class foo_decorator(object): 
    def __init__(self, decoratee): 
     self._decoratee = decoratee 
    def f1(self): 
     print "decorated f1" 
     self._decoratee.f1() 
    def f2(self):    # I would like to leave that part out 
     self._decoratee.f2() 

Mi piacerebbe avere chiamate a foo_decorator.f2 inoltrate decoratee.f2 automaticamente. C'è un modo per scrivere un metodo generico che inoltra tutte le chiamate di funzioni non implementate a decoratee?

+1

Puoi fare un esempio in cui semplicemente la sottoclasse non funzionerebbe? Puoi anche sottoclassi dinamicamente - questo modello sembra una soluzione alternativa per le lingue che non possono farlo o che non supportano più inheritage. –

+0

Voglio decorare oggetti in fase di esecuzione. Voglio applicare diversi decoratori a un oggetto ed essere in grado di rimuoverli di nuovo. La sottoclasse non può cambiare un'istanza dopo che è stata creata, o può? –

+0

Se avete la classe 'A' e cambiate' A', cioè aggiungendo un nuovo metodo, 'A.foo = lambda self: self' questo si rifletterà su tutte le istanze di A .. perché * tutto * è determinato in fase di esecuzione. Ottimo modo per produrre codice assolutamente non gestibile. –

risposta

30

si potrebbe usare __getattr__:

class foo(object): 
    def f1(self): 
     print "original f1" 
    def f2(self): 
     print "original f2" 

class foo_decorator(object): 
    def __init__(self, decoratee): 
     self._decoratee = decoratee 
    def f1(self): 
     print "decorated f1" 
     self._decoratee.f1() 
    def __getattr__(self, name): 
     return getattr(self._decoratee, name) 

u = foo() 
v = foo_decorator(u) 
v.f1() 
v.f2() 
+1

Questa è una grande idea. Va notato, tuttavia, che se si utilizzano le classi di base astratte (ad es. Importare abc e utilizzare __metaclass__ = abc.ABCMeta per definire metodi e proprietà astratti), allora non funzionerà. – abergou

+0

Questo non funziona per funzioni come __str__. Cosa sta succedendo lì? –

+0

@JohnKitchin: il reindirizzamento non funziona per i metodi di doppia sottolineatura, poiché questi devono (per motivi di prestazioni) essere definiti sulla classe, non sull'istanza. [(maggiori informazioni)] (https://docs.python.org/3/reference/datamodel.html#special-lookup) Credo che l'unica soluzione alternativa sia definire e reindirizzare esplicitamente metodi come '__str__'. –

2

non è probabilmente la migliore prassi, ma è possibile aggiungere funzionalità alle istanze, come ho fatto per aiutare la transizione il mio codice da ORM di Django per SQLAlachemy, come segue:

def _save(self): 
    session.add(self) 
    session.commit() 
setattr(Base,'save',_save) 
+1

Ho pensato anche a questo, ma mi sembra molto sbagliato. Forse perché vengo dal C++? –

+2

È giusto sentirsi in errore a riguardo. Potrebbe essere considerato un patch per scimmia. Tuttavia funziona bene con un piccolo codice, è un mondo molto diverso con più possibilità di C quando tutto è dinamico e di riferimento. – andyortlieb

+0

Mi piace. Per questo esempio, qualsiasi altra cosa introdurrebbe solo più complessità IMO. – Chomeh

1

Il diagramma UML nell'articolo di Wikipedia collegato è errato e lo è anche il tuo codice.

Se segui il "motivo decoratore", la classe decoratore deriva dalla classe decorata di base. (Nel diagramma UML manca una freccia di eredità da WindowDecorator a Window).

con

class foo_decorator(foo): 

non è necessario implementare metodi non decorati.

BTW: In forte lingue digitati c'è una ragione in più, per cui il decoratore deve essere derivato dalla classe decorato: In caso contrario, si wouldnt essere in grado di decoratori catena.

+0

Il diagramma UML mostra sia l'ereditarietà che l'aggregazione (due parti essenziali per il modello decoratore) della classe base.Si eredita dalla classe base, quindi si assomiglia all'originale e si mantiene un riferimento a un'istanza della classe base in cui è possibile isolare l'accesso ad essa. L'articolo di wikipedia lo dice nei passaggi 1 e 2: "(1) Sottoclasse del componente originale, (2) aggiungi un puntatore Componente come campo". Quindi la domanda originale è in realtà sulla tipizzazione delle anteprime di Python, e nemmeno sul modo in cui i decoratori hanno scalpore, né sui decoratori di Python! – maxpolk

8

Come addendum alla risposta di Philipp; se è necessario non solo per decorare, ma preservare la tipo di un oggetto, Python consente di sottoclasse un'istanza in fase di esecuzione:

class foo(object): 
    def f1(self): 
     print "original f1" 

    def f2(self): 
     print "original f2" 


class foo_decorator(object): 
    def __new__(cls, decoratee): 
     cls = type('decorated', 
        (foo_decorator, decoratee.__class__), 
        decoratee.__dict__) 
     return object.__new__(cls) 

    def f1(self): 
     print "decorated f1" 
     super(foo_decorator, self).f1() 


u = foo() 
v = foo_decorator(u) 
v.f1() 
v.f2() 
print 'isinstance(v, foo) ==', isinstance(v, foo) 

Questo è un po 'più coinvolto di quanto strettamente necessario per il tuo esempio, in cui si sapere che la classe viene decorata in anticipo

Questo potrebbe bastano:

class foo_decorator(foo): 
    def __init__(self, decoratee): 
     self.__dict__.update(decoratee.__dict__) 

    def f1(self): 
     print "decorated f1" 
     super(foo_decorator, self).f1() 
0

Per completare @Alec Thomas risposta. Ho modificato la sua risposta per seguire lo schema del decoratore. In questo modo non è necessario conoscere in anticipo la classe che stai decorando.

class Decorator(object): 
    def __new__(cls, decoratee): 
     cls = type('decorated', 
        (cls, decoratee.__class__), 
        decoratee.__dict__) 
     return object.__new__(cls) 

Quindi, è possibile utilizzarlo come:

class SpecificDecorator(Decorator): 
    def f1(self): 
     print "decorated f1" 
     super(foo_decorator, self).f1() 

class Decorated(object): 
    def f1(self): 
     print "original f1" 


d = SpecificDecorator(Decorated()) 
d.f1() 
0

In uno dei miei progetti, ho anche bisogno di fare una cosa in particolare, vale a dire che anche l'oggetto sottostante dovrebbe realmente eseguire il metodo che è stato reimplementato nel decoratore. In realtà è abbastanza facile da fare se sai dove indirizzarlo.

Il caso di utilizzo è:

  • Ho un oggetto X con metodi A e B.
  • Creo una classe decoratore Y che sostituisce A.
  • Se si crea un'istanza Y (X) e si chiama A, verrà utilizzata la A decorata come previsto.
  • Se B chiama A, quindi se istanzia Y (X) e chiama B sul decoratore, la chiamata dall'interno di B quindi passa alla vecchia A dell'oggetto originale che non era desiderabile. Voglio che anche la vecchia B chiami la nuova A.

E 'possibile raggiungere questo comportamento come questo:

import inspect 
import six  # for handling 2-3 compatibility 

class MyBaseDecorator(object): 
    def __init__(self, decorated): 
     self.decorated = decorated 

    def __getattr__(self, attr): 
     value = getattr(self.decorated, attr) 
     if inspect.ismethod(value): 
      function = six.get_method_function(value) 
      value = function.__get__(self, type(self)) 
     return value 

class SomeObject(object): 
    def a(self): 
     pass 

    def b(self): 
     pass 

class MyDecorator(MyBaseDecorator): 
    def a(self): 
     pass 

decorated = MyDecorator(SomeObject()) 

Questo non può funzionare out of the box, come ho digitato tutto il resto a parte il metodo di getattr dalla parte superiore della testa.

Il codice cerca l'attributo richiesto nell'oggetto decorato e se è un metodo (non funziona per le proprietà ora, ma la modifica per supportarle non dovrebbe essere troppo difficile), il codice quindi attira l'effettivo funzione fuori dal metodo e usando l'invocazione dell'interfaccia del descrittore "ricollega" la funzione come metodo, ma sul decoratore. Quindi viene restituito e molto probabilmente eseguito.

L'effetto di questo è che se b chiama sempre a sull'oggetto originale, quindi quando l'oggetto è decorato e c'è una chiamata di metodo proveniente dal decoratore, il decoratore si assicura che tutti i metodi a cui si accede siano vincolati al decoratore invece, quindi cercare le cose usando il decoratore e non l'oggetto originale, quindi i metodi specificati nel decoratore hanno la precedenza.

P.S .: Sì, lo so che assomiglia molto all'eredità, ma questo nel senso della composizione di più oggetti.

Problemi correlati