Un decoratore di un metodo di istanza può accedere alla classe?


109

Ho qualcosa di simile al seguente. Fondamentalmente ho bisogno di accedere alla classe di un metodo di istanza da un decoratore utilizzato sul metodo di istanza nella sua definizione.

def decorator(view):
    # do something that requires view's class
    print view.im_class
    return view

class ModelA(object):
    @decorator
    def a_method(self):
        # do some stuff
        pass

Il codice così com'è fornisce:

AttributeError: l'oggetto "function" non ha attributo "im_class"

Ho trovato domande / risposte simili: Python decorator fa dimenticare alla funzione di appartenere a una classe e Get class in Python decorator , ma queste si basano su una soluzione alternativa che acquisisce l'istanza in fase di esecuzione strappando il primo parametro. Nel mio caso, chiamerò il metodo in base alle informazioni raccolte dalla sua classe, quindi non vedo l'ora che arrivi una chiamata.

Risposte:


68

Se stai usando Python 2.6 o successivo potresti usare un decoratore di classi, forse qualcosa del genere (attenzione: codice non testato).

def class_decorator(cls):
   for name, method in cls.__dict__.iteritems():
        if hasattr(method, "use_class"):
            # do something with the method and class
            print name, cls
   return cls

def method_decorator(view):
    # mark the method as something that requires view's class
    view.use_class = True
    return view

@class_decorator
class ModelA(object):
    @method_decorator
    def a_method(self):
        # do some stuff
        pass

Il decoratore di metodi contrassegna il metodo come interessante aggiungendo un attributo "use_class": anche le funzioni ei metodi sono oggetti, quindi puoi allegare loro metadati aggiuntivi.

Dopo che la classe è stata creata, il decoratore della classe passa attraverso tutti i metodi e fa tutto ciò che è necessario sui metodi che sono stati contrassegnati.

Se vuoi che tutti i metodi siano influenzati, puoi tralasciare il decoratore del metodo e usare semplicemente il decoratore della classe.


2
Grazie credo che questa sia la via con cui andare. Solo una riga di codice in più per ogni classe in cui vorrei usare questo decoratore. Forse potrei usare una metaclasse personalizzata ed eseguire lo stesso controllo durante il nuovo ...?
Carl G

3
Chiunque cerchi di usarlo con staticmethod o classmethod vorrà leggere questo PEP: python.org/dev/peps/pep-0232 Non sono sicuro che sia possibile perché non puoi impostare un attributo su un metodo di classe / statico e penso che divorino su qualsiasi attributo di funzione personalizzato quando vengono applicati a una funzione.
Carl G

Proprio quello che stavo cercando, per il mio ORM basato su DBM ... Grazie, amico.
Coyote21

Utilizzare inspect.getmro(cls)per elaborare tutte le classi di base nel decoratore di classi per supportare l'ereditarietà.
schlamar

1
Oh, in realtà sembra che inspectper il salvataggio stackoverflow.com/a/1911287/202168
Anentropic

16

A partire da python 3.6 puoi usare object.__set_name__per ottenere ciò in un modo molto semplice. Il documento afferma che __set_name__"viene chiamato al momento della creazione del proprietario della classe proprietaria ". Ecco un esempio:

class class_decorator:
    def __init__(self, fn):
        self.fn = fn

    def __set_name__(self, owner, name):
        # do something with owner, i.e.
        print(f"decorating {self.fn} and using {owner}")
        self.fn.class_name = owner.__name__

        # then replace ourself with the original method
        setattr(owner, name, self.fn)

Si noti che viene chiamato al momento della creazione della classe:

>>> class A:
...     @class_decorator
...     def hello(self, x=42):
...         return x
...
decorating <function A.hello at 0x7f9bedf66bf8> and using <class '__main__.A'>
>>> A.hello
<function __main__.A.hello(self, x=42)>
>>> A.hello.class_name
'A'
>>> a = A()
>>> a.hello()
42

Se vuoi saperne di più su come vengono create le classi e in particolare esattamente quando __set_name__viene chiamata, puoi fare riferimento alla documentazione su "Creazione dell'oggetto classe" .


1
Come sarebbe usare il decoratore con i parametri? Ad esempio@class_decorator('test', foo='bar')
luckydonald

2
@luckydonald Puoi avvicinarti in modo simile ai normali decoratori che accettano argomenti . Basta averedef decorator(*args, **kwds): class Descriptor: ...; return Descriptor
Matt Eding

Wow, grazie mille. Non lo sapevo, __set_name__anche se utilizzo Python 3.6+ da molto tempo.
kawing-chiu

C'è uno svantaggio di questo metodo: il controllo statico non lo capisce affatto. Mypy penserà che hellonon è un metodo, ma piuttosto un oggetto di tipo class_decorator.
kawing-chiu

@ kawing-chiu Se nient'altro funziona, puoi usare un if TYPE_CHECKINGper definire class_decoratorcome un normale decoratore che restituisce il tipo corretto.
tyrion

15

Come altri hanno sottolineato, la classe non è stata creata nel momento in cui viene chiamato il decoratore. Tuttavia , è possibile annotare l'oggetto funzione con i parametri del decoratore, quindi decorare nuovamente la funzione nel __new__metodo della metaclasse . Avrai bisogno di accedere __dict__direttamente all'attributo della funzione , come almeno per me, ha func.foo = 1provocato un AttributeError.


6
setattrdovrebbe essere usato invece di accedere__dict__
schlamar

7

Come suggerisce Mark:

  1. Qualsiasi decoratore è chiamato PRIMA che la classe sia costruita, quindi è sconosciuto al decoratore.
  2. Possiamo taggare questi metodi ed eseguire in un secondo momento qualsiasi post-elaborazione necessaria.
  3. Abbiamo due opzioni per la post-elaborazione: automaticamente alla fine della definizione della classe o da qualche parte prima che l'applicazione venga eseguita. Preferisco la prima opzione utilizzando una classe base, ma puoi anche seguire il secondo approccio.

Questo codice mostra come funziona utilizzando la post-elaborazione automatica:

def expose(**kw):
    "Note that using **kw you can tag the function with any parameters"
    def wrap(func):
        name = func.func_name
        assert not name.startswith('_'), "Only public methods can be exposed"

        meta = func.__meta__ = kw
        meta['exposed'] = True
        return func

    return wrap

class Exposable(object):
    "Base class to expose instance methods"
    _exposable_ = None  # Not necessary, just for pylint

    class __metaclass__(type):
        def __new__(cls, name, bases, state):
            methods = state['_exposed_'] = dict()

            # inherit bases exposed methods
            for base in bases:
                methods.update(getattr(base, '_exposed_', {}))

            for name, member in state.items():
                meta = getattr(member, '__meta__', None)
                if meta is not None:
                    print "Found", name, meta
                    methods[name] = member
            return type.__new__(cls, name, bases, state)

class Foo(Exposable):
    @expose(any='parameter will go', inside='__meta__ func attribute')
    def foo(self):
        pass

class Bar(Exposable):
    @expose(hide=True, help='the great bar function')
    def bar(self):
        pass

class Buzz(Bar):
    @expose(hello=False, msg='overriding bar function')
    def bar(self):
        pass

class Fizz(Foo):
    @expose(msg='adding a bar function')
    def bar(self):
        pass

print('-' * 20)
print("showing exposed methods")
print("Foo: %s" % Foo._exposed_)
print("Bar: %s" % Bar._exposed_)
print("Buzz: %s" % Buzz._exposed_)
print("Fizz: %s" % Fizz._exposed_)

print('-' * 20)
print('examine bar functions')
print("Bar.bar: %s" % Bar.bar.__meta__)
print("Buzz.bar: %s" % Buzz.bar.__meta__)
print("Fizz.bar: %s" % Fizz.bar.__meta__)

L'output produce:

Found foo {'inside': '__meta__ func attribute', 'any': 'parameter will go', 'exposed': True}
Found bar {'hide': True, 'help': 'the great bar function', 'exposed': True}
Found bar {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Found bar {'msg': 'adding a bar function', 'exposed': True}
--------------------
showing exposed methods
Foo: {'foo': <function foo at 0x7f7da3abb398>}
Bar: {'bar': <function bar at 0x7f7da3abb140>}
Buzz: {'bar': <function bar at 0x7f7da3abb0c8>}
Fizz: {'foo': <function foo at 0x7f7da3abb398>, 'bar': <function bar at 0x7f7da3abb488>}
--------------------
examine bar functions
Bar.bar: {'hide': True, 'help': 'the great bar function', 'exposed': True}
Buzz.bar: {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Fizz.bar: {'msg': 'adding a bar function', 'exposed': True}

Nota che in questo esempio:

  1. Possiamo annotare qualsiasi funzione con qualsiasi parametro arbitrario.
  2. Ogni classe ha i propri metodi esposti.
  3. Possiamo ereditare anche metodi esposti.
  4. i metodi possono essere sovrascritti quando la funzione di esposizione viene aggiornata.

Spero che questo ti aiuti


4

Come indicato da Ants, non puoi ottenere un riferimento alla classe dall'interno della classe. Tuttavia, se sei interessato a distinguere tra classi diverse (senza manipolare l'oggetto del tipo di classe effettivo), puoi passare una stringa per ogni classe. Puoi anche passare qualsiasi altro parametro che desideri al decoratore usando decoratori in stile classe.

class Decorator(object):
    def __init__(self,decoratee_enclosing_class):
        self.decoratee_enclosing_class = decoratee_enclosing_class
    def __call__(self,original_func):
        def new_function(*args,**kwargs):
            print 'decorating function in ',self.decoratee_enclosing_class
            original_func(*args,**kwargs)
        return new_function


class Bar(object):
    @Decorator('Bar')
    def foo(self):
        print 'in foo'

class Baz(object):
    @Decorator('Baz')
    def foo(self):
        print 'in foo'

print 'before instantiating Bar()'
b = Bar()
print 'calling b.foo()'
b.foo()

stampe:

before instantiating Bar()
calling b.foo()
decorating function in  Bar
in foo

Inoltre, vedi la pagina di Bruce Eckel sui decoratori.


Grazie per aver confermato la mia deprimente conclusione che questo non è possibile. Potrei anche usare una stringa che qualifichi completamente il modulo / classe ('module.Class'), memorizzare le stringhe fino a quando le classi non sono state caricate completamente, quindi recuperare le classi da solo con l'importazione. Sembra un modo tristemente non SECCO per portare a termine il mio compito.
Carl G

Non è necessario utilizzare una classe per questo tipo di decoratore: l'approccio idiomatico consiste nell'usare un livello extra di funzioni annidate all'interno della funzione decoratore. Tuttavia, se vai con le classi, potrebbe essere meglio non usare le maiuscole nel nome della classe per far sembrare la decorazione stessa "standard", cioè @decorator('Bar')al contrario di @Decorator('Bar').
Erik Kaplun

4

Quello che fa flask-classy è creare una cache temporanea che memorizza sul metodo, quindi utilizza qualcos'altro (il fatto che Flask registrerà le classi utilizzando un registermetodo di classe) per avvolgere effettivamente il metodo.

Puoi riutilizzare questo modello, questa volta usando una metaclasse in modo da poter avvolgere il metodo al momento dell'importazione.

def route(rule, **options):
    """A decorator that is used to define custom routes for methods in
    FlaskView subclasses. The format is exactly the same as Flask's
    `@app.route` decorator.
    """

    def decorator(f):
        # Put the rule cache on the method itself instead of globally
        if not hasattr(f, '_rule_cache') or f._rule_cache is None:
            f._rule_cache = {f.__name__: [(rule, options)]}
        elif not f.__name__ in f._rule_cache:
            f._rule_cache[f.__name__] = [(rule, options)]
        else:
            f._rule_cache[f.__name__].append((rule, options))

        return f

    return decorator

Sulla classe attuale (potresti fare lo stesso usando una metaclasse):

@classmethod
def register(cls, app, route_base=None, subdomain=None, route_prefix=None,
             trailing_slash=None):

    for name, value in members:
        proxy = cls.make_proxy_method(name)
        route_name = cls.build_route_name(name)
        try:
            if hasattr(value, "_rule_cache") and name in value._rule_cache:
                for idx, cached_rule in enumerate(value._rule_cache[name]):
                    # wrap the method here

Fonte: https://github.com/apiguy/flask-classy/blob/master/flask_classy.py


questo è un modello utile, ma non risolve il problema di un decoratore di metodi che è in grado di fare riferimento alla classe genitore del metodo a cui è applicato
Anentropic

Ho aggiornato la mia risposta per essere più esplicito su come questo possa essere utile per ottenere l'accesso alla classe al momento dell'importazione (cioè usando una metaclasse + memorizzando nella cache il parametro decorator sul metodo).
charlax

3

Il problema è che quando viene chiamato il decoratore la classe non esiste ancora. Prova questo:

def loud_decorator(func):
    print("Now decorating %s" % func)
    def decorated(*args, **kwargs):
        print("Now calling %s with %s,%s" % (func, args, kwargs))
        return func(*args, **kwargs)
    return decorated

class Foo(object):
    class __metaclass__(type):
        def __new__(cls, name, bases, dict_):
            print("Creating class %s%s with attributes %s" % (name, bases, dict_))
            return type.__new__(cls, name, bases, dict_)

    @loud_decorator
    def hello(self, msg):
        print("Hello %s" % msg)

Foo().hello()

Questo programma produrrà:

Now decorating <function hello at 0xb74d35dc>
Creating class Foo(<type 'object'>,) with attributes {'__module__': '__main__', '__metaclass__': <class '__main__.__metaclass__'>, 'hello': <function decorated at 0xb74d356c>}
Now calling <function hello at 0xb74d35dc> with (<__main__.Foo object at 0xb74ea1ac>, 'World'),{}
Hello World

Come vedi, dovrai trovare un modo diverso per fare quello che vuoi.


quando si definisce una funzione la funzione non esiste ancora, ma si è in grado di chiamare ricorsivamente la funzione dall'interno di se stessa. Immagino che questa sia una caratteristica del linguaggio specifica per le funzioni e non disponibile per le classi.
Carl G

DGGenuine: la funzione viene chiamata solo e la funzione accede quindi a se stessa solo dopo essere stata creata completamente. In questo caso, la classe non può essere completa quando viene chiamato il decoratore, poiché la classe deve attendere il risultato del decoratore, che verrà memorizzato come uno degli attributi della classe.
u0b34a0f6ae

3

Ecco un semplice esempio:

def mod_bar(cls):
    # returns modified class

    def decorate(fcn):
        # returns decorated function

        def new_fcn(self):
            print self.start_str
            print fcn(self)
            print self.end_str

        return new_fcn

    cls.bar = decorate(cls.bar)
    return cls

@mod_bar
class Test(object):
    def __init__(self):
        self.start_str = "starting dec"
        self.end_str = "ending dec" 

    def bar(self):
        return "bar"

L'output è:

>>> import Test
>>> a = Test()
>>> a.bar()
starting dec
bar
ending dec

1

Questa è una vecchia domanda, ma mi sono imbattuto in venusiano. http://venusian.readthedocs.org/en/latest/

Sembra che abbia la capacità di decorare i metodi e di darti accesso sia alla classe che al metodo mentre lo fai. Nota che chiamare setattr(ob, wrapped.__name__, decorated)non è il modo tipico di usare la venusiana e in qualche modo vanifica lo scopo.

Ad ogni modo ... l'esempio seguente è completo e dovrebbe essere eseguito.

import sys
from functools import wraps
import venusian

def logged(wrapped):
    def callback(scanner, name, ob):
        @wraps(wrapped)
        def decorated(self, *args, **kwargs):
            print 'you called method', wrapped.__name__, 'on class', ob.__name__
            return wrapped(self, *args, **kwargs)
        print 'decorating', '%s.%s' % (ob.__name__, wrapped.__name__)
        setattr(ob, wrapped.__name__, decorated)
    venusian.attach(wrapped, callback)
    return wrapped

class Foo(object):
    @logged
    def bar(self):
        print 'bar'

scanner = venusian.Scanner()
scanner.scan(sys.modules[__name__])

if __name__ == '__main__':
    t = Foo()
    t.bar()

1

La funzione non sa se si tratta di un metodo nel punto di definizione, quando viene eseguito il codice del decoratore. Solo quando si accede tramite l'identificatore di classe / istanza, può conoscere la sua classe / istanza. Per superare questa limitazione, è possibile decorare per oggetto descrittore per ritardare il codice di decorazione effettivo fino all'ora di accesso / chiamata:

class decorated(object):
    def __init__(self, func, type_=None):
        self.func = func
        self.type = type_

    def __get__(self, obj, type_=None):
        func = self.func.__get__(obj, type_)
        print('accessed %s.%s' % (type_.__name__, func.__name__))
        return self.__class__(func, type_)

    def __call__(self, *args, **kwargs):
        name = '%s.%s' % (self.type.__name__, self.func.__name__)
        print('called %s with args=%s kwargs=%s' % (name, args, kwargs))
        return self.func(*args, **kwargs)

Ciò consente di decorare metodi individuali (statici | classe):

class Foo(object):
    @decorated
    def foo(self, a, b):
        pass

    @decorated
    @staticmethod
    def bar(a, b):
        pass

    @decorated
    @classmethod
    def baz(cls, a, b):
        pass

class Bar(Foo):
    pass

Ora puoi usare il codice del decoratore per l'introspezione ...

>>> Foo.foo
accessed Foo.foo
>>> Foo.bar
accessed Foo.bar
>>> Foo.baz
accessed Foo.baz
>>> Bar.foo
accessed Bar.foo
>>> Bar.bar
accessed Bar.bar
>>> Bar.baz
accessed Bar.baz

... e per modificare il comportamento della funzione:

>>> Foo().foo(1, 2)
accessed Foo.foo
called Foo.foo with args=(1, 2) kwargs={}
>>> Foo.bar(1, b='bcd')
accessed Foo.bar
called Foo.bar with args=(1,) kwargs={'b': 'bcd'}
>>> Bar.baz(a='abc', b='bcd')
accessed Bar.baz
called Bar.baz with args=() kwargs={'a': 'abc', 'b': 'bcd'}

Purtroppo, questo approccio è funzionalmente equivalente a Will McCutchen 's risposta altrettanto inapplicabile . Sia questa che quella risposta ottengono la classe desiderata al momento della chiamata del metodo piuttosto che il tempo di decorazione del metodo , come richiesto dalla domanda originale. L'unico mezzo ragionevole per ottenere questa classe in un tempo sufficientemente precoce è quello di esaminare tutti i metodi al momento della definizione della classe (ad esempio, tramite un decoratore di classi o metaclasse). </sigh>
Cecil Curry,

1

Come altre risposte hanno sottolineato, decorator è una cosa funzionale, non puoi accedere alla classe a cui appartiene questo metodo poiché la classe non è stata ancora creata. Tuttavia, è assolutamente corretto utilizzare un decoratore per "contrassegnare" la funzione e quindi utilizzare le tecniche della metaclasse per gestire il metodo in un secondo momento, perché nella __new__fase, la classe è stata creata dalla sua metaclasse.

Qui c'è un semplice esempio:

Usiamo @fieldper contrassegnare il metodo come un campo speciale e trattarlo in metaclasse.

def field(fn):
    """Mark the method as an extra field"""
    fn.is_field = True
    return fn

class MetaEndpoint(type):
    def __new__(cls, name, bases, attrs):
        fields = {}
        for k, v in attrs.items():
            if inspect.isfunction(v) and getattr(k, "is_field", False):
                fields[k] = v
        for base in bases:
            if hasattr(base, "_fields"):
                fields.update(base._fields)
        attrs["_fields"] = fields

        return type.__new__(cls, name, bases, attrs)

class EndPoint(metaclass=MetaEndpoint):
    pass


# Usage

class MyEndPoint(EndPoint):
    @field
    def foo(self):
        return "bar"

e = MyEndPoint()
e._fields  # {"foo": ...}

Hai un errore di battitura in questa riga: if inspect.isfunction(v) and getattr(k, "is_field", False):dovrebbe essere getattr(v, "is_field", False)invece.
EvilTosha

0

Avrai accesso alla classe dell'oggetto su cui il metodo viene chiamato nel metodo decorato che il tuo decoratore dovrebbe restituire. Così:

def decorator(method):
    # do something that requires view's class
    def decorated(self, *args, **kwargs):
        print 'My class is %s' % self.__class__
        method(self, *args, **kwargs)
    return decorated

Usando la tua classe ModelA, ecco cosa fa:

>>> obj = ModelA()
>>> obj.a_method()
My class is <class '__main__.ModelA'>

1
Grazie, ma questa è esattamente la soluzione a cui ho fatto riferimento nella mia domanda che non funziona per me. Sto cercando di implementare un pattern di osservazione usando decoratori e non sarò mai in grado di chiamare il metodo nel contesto corretto dal mio dispatcher di osservazioni se non ho la classe a un certo punto mentre aggiungo il metodo al dispatcher di osservazioni. Ottenere la classe dopo la chiamata al metodo non mi aiuta a chiamare correttamente il metodo in primo luogo.
Carl G

Whoa, scusa per la mia pigrizia nel non leggere l'intera domanda.
Will McCutchen

0

Voglio solo aggiungere il mio esempio poiché contiene tutte le cose a cui potrei pensare per accedere alla classe dal metodo decorato. Usa un descrittore come suggerisce @tyrion. Il decoratore può prendere argomenti e passarli al descrittore. Può gestire sia un metodo in una classe che una funzione senza una classe.

import datetime as dt
import functools

def dec(arg1):
    class Timed(object):
        local_arg = arg1
        def __init__(self, f):
            functools.update_wrapper(self, f)
            self.func = f

        def __set_name__(self, owner, name):
            # doing something fancy with owner and name
            print('owner type', owner.my_type())
            print('my arg', self.local_arg)

        def __call__(self, *args, **kwargs):
            start = dt.datetime.now()
            ret = self.func(*args, **kwargs)
            time = dt.datetime.now() - start
            ret["time"] = time
            return ret
        
        def __get__(self, instance, owner):
            from functools import partial
            return partial(self.__call__, instance)
    return Timed

class Test(object):
    def __init__(self):
        super(Test, self).__init__()

    @classmethod
    def my_type(cls):
        return 'owner'

    @dec(arg1='a')
    def decorated(self, *args, **kwargs):
        print(self)
        print(args)
        print(kwargs)
        return dict()

    def call_deco(self):
        self.decorated("Hello", world="World")

@dec(arg1='a function')
def another(*args, **kwargs):
    print(args)
    print(kwargs)
    return dict()

if __name__ == "__main__":
    t = Test()
    ret = t.call_deco()
    another('Ni hao', world="shi jie")
    
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.