Quali sono le differenze tra un metodo di classe e un metodo metaclasse?


12

In Python, posso creare un metodo di classe usando il @classmethoddecoratore:

>>> class C:
...     @classmethod
...     def f(cls):
...             print(f'f called with cls={cls}')
...
>>> C.f()
f called with cls=<class '__main__.C'>

In alternativa, posso usare un metodo normale (istanza) su una metaclasse:

>>> class M(type):
...     def f(cls):
...             print(f'f called with cls={cls}')
...
>>> class C(metaclass=M):
...     pass
...
>>> C.f()
f called with cls=<class '__main__.C'>

Come dimostrato dall'output di C.f(), questi due approcci offrono funzionalità simili.

Quali sono le differenze tra l'utilizzo @classmethode l'uso di un metodo normale su una metaclasse?

Risposte:


5

Poiché le classi sono istanze di una metaclasse, non è inaspettato che un "metodo di istanza" sulla metaclasse si comporti come un metodo di classe.

Tuttavia, sì, ci sono differenze - e alcune sono più che semantiche:

  1. La differenza più importante è che un metodo nella metaclasse non è "visibile" da un'istanza di classe . Ciò accade perché la ricerca dell'attributo in Python (in modo semplificato - i descrittori possono avere la precedenza) cerca un attributo nell'istanza - se non è presente nell'istanza, Python cerca quindi nella classe di quell'istanza e quindi la ricerca continua le superclassi della classe, ma non sulle classi della classe. Lo stdlib di Python utilizza questa funzione nel abc.ABCMeta.registermetodo. Tale funzionalità può essere utilizzata per sempre, poiché i metodi relativi alla classe stessa sono liberi di essere riutilizzati come attributi di istanza senza alcun conflitto (ma un metodo sarebbe comunque in conflitto).
  2. Un'altra differenza, sebbene ovvia, è che un metodo dichiarato nella metaclasse può essere disponibile in diverse classi, non altrimenti correlate - se si dispone di gerarchie di classi diverse, non correlate affatto in ciò che trattano, ma si desidera alcune funzionalità comuni per tutte le classi , dovresti inventare una classe mixin, che dovrebbe essere inclusa come base in entrambe le gerarchie (diciamo per includere tutte le classi in un registro dell'applicazione). (NB: il mixin a volte può essere una chiamata migliore di una metaclasse)
  3. Un classmethod è un oggetto "classmethod" specializzato, mentre un metodo nella metaclasse è una funzione ordinaria.

Quindi, accade che il meccanismo usato dai metodi di classe sia il " protocollo descrittore ". Mentre le normali funzioni presentano un __get__metodo che inserirà l' selfargomento quando vengono recuperate da un'istanza e lascerà vuoto tale argomento quando recuperato da una classe, un classmethodoggetto ha un diverso __get__, che inserirà la classe stessa (il "proprietario") come primo parametro in entrambe le situazioni.

Ciò non fa differenze pratiche la maggior parte delle volte, ma se si desidera accedere al metodo come funzione, allo scopo di aggiungere dinamicamente l'aggiunta di decoratore ad esso, o qualsiasi altro, poiché un metodo nella metaclasse meta.methodrecupera la funzione, pronto per essere utilizzato , mentre devi usarlo cls.my_classmethod.__func__ per recuperarlo da un metodo di classe (e quindi devi creare un altro classmethodoggetto e assegnarlo di nuovo, se esegui un wrapping).

Fondamentalmente, questi sono i 2 esempi:


class M1(type):
    def clsmethod1(cls):
        pass

class CLS1(metaclass=M1):
    pass

def runtime_wrap(cls, method_name, wrapper):
    mcls = type(cls)
    setattr(mcls, method_name,  wrapper(getatttr(mcls, method_name)))

def wrapper(classmethod):
    def new_method(cls):
        print("wrapper called")
        return classmethod(cls)
    return new_method

runtime_wrap(cls1, "clsmethod1", wrapper)

class CLS2:
    @classmethod
    def classmethod2(cls):
        pass

 def runtime_wrap2(cls, method_name, wrapper):
    setattr(cls, method_name,  classmethod(
                wrapper(getatttr(cls, method_name).__func__)
        )
    )

runtime_wrap2(cls1, "clsmethod1", wrapper)

In altre parole: a parte l'importante differenza che un metodo definito nella metaclasse è visibile dall'istanza e un classmethodoggetto no, le altre differenze in fase di esecuzione sembreranno oscure e insignificanti, ma ciò accade perché il linguaggio non ha bisogno di andare fuori dalla sua strada con regole speciali per i metodi di classe: entrambi i modi di dichiarare un metodo di classe sono possibili, come conseguenza del design del linguaggio - uno, per il fatto che una classe è essa stessa un oggetto, e un'altra, come possibilità tra molti, di l'uso del protocollo descrittore che consente di specializzare l'accesso agli attributi in un'istanza e in una classe:

Il classmethodbuiltin è definito in codice nativo, ma potrebbe essere codificato in puro pitone e funzionerebbe esattamente allo stesso modo. Il soffietto di classe a 5 righe può essere usato come classmethoddecoratore senza differenze di runtime rispetto al isinstance @classmethod" at all (though distinguishable through introspection such as calls torepr` , and evenintegrato ovviamente):


class myclassmethod:
    def __init__(self, func):
        self.__func__ = func
    def __get__(self, instance, owner):
        return lambda *args, **kw: self.__func__(owner, *args, **kw)

E, al di là dei metodi, è interessante tenere presente che attributi specializzati come a @propertysulla metaclasse funzioneranno come attributi di classe specializzati, lo stesso, senza alcun comportamento sorprendente.


2

Quando lo pronunci come hai fatto nella domanda, i @classmethodmetaclassi e possono apparire simili ma hanno scopi piuttosto diversi. La classe che viene iniettata @classmethodnell'argomento di solito viene usata per costruire un'istanza (cioè un costruttore alternativo). D'altra parte, i metaclassi vengono generalmente utilizzati per modificare la classe stessa (ad esempio, come fa Django con i suoi modelli DSL).

Questo non vuol dire che non è possibile modificare la classe all'interno di un metodo di classe. Ma poi la domanda diventa perché non hai definito la classe nel modo in cui vuoi modificarla in primo luogo? Altrimenti, potrebbe suggerire a un refattore di utilizzare più classi.

Espandiamo un po 'il primo esempio.

class C:
    @classmethod
    def f(cls):
        print(f'f called with cls={cls}')

Prendendo in prestito dai documenti di Python , quanto sopra si espanderà a qualcosa di simile al seguente:

class ClassMethod(object):
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc

class C:
    def f(cls):
        print(f'f called with cls={cls}')
    f = ClassMethod(f)

Nota come __get__può prendere un'istanza o la classe (o entrambe), quindi puoi fare entrambe C.fe C().f. Questo è diverso l'esempio metaclasse si dà, che lancerà un AttributeErrorper C().f.

Inoltre, nell'esempio di metaclasse, fnon esiste in C.__dict__. Quando si guarda l'attributo fcon C.f, l'interprete guarda C.__dict__e poi, dopo aver fallito nel trovare, guarda type(C).__dict__(che è M.__dict__). Questo può importa se si desidera che la flessibilità necessaria per ignorare fin C, anche se dubito che questo potrà mai essere di uso pratico.


0

Nel tuo esempio, la differenza sarebbe in alcune altre classi con M impostato come metaclasse.

class M(type):
    def f(cls):
        pass

class C(metaclass=M):
    pass

class C2(metaclass=M):
    pass

C.f()
C2.f()
class M(type):
     pass

class C(metaclass=M):
     @classmethod
     def f(cls):
        pass

class C2(metaclass=M):
    pass

C.f()
# C2 does not have 'f'

Ecco di più sui metaclassi Quali sono alcuni (concreti) casi d'uso per i metaclassi?


0

Sia @classmethod che Metaclass sono diversi.

Tutto in Python è un oggetto. Ogni cosa significa ogni cosa.

Che cos'è Metaclass?

Come detto ogni cosa è un oggetto. Le classi sono anche oggetti, infatti le classi sono istanze di altri oggetti misteriosi chiamati formalmente come meta-classi. La metaclasse predefinita in Python è "type" se non specificata

Per impostazione predefinita, tutte le classi definite sono istanze di tipo.

Le classi sono istanze di Meta-Classi

Pochi punti importanti sono capire il comportamento menzionato

  • Poiché le classi sono istanze di meta-classi.
  • Come ogni oggetto istanziato, come gli oggetti (istanze) ottengono i loro attributi dalla classe. La classe otterrà i suoi attributi da Meta-Class

Considera il seguente codice

class Meta(type):
    def foo(self):
        print(f'foo is called self={self}')
        print('{} is instance of {}: {}'.format(self, Meta, isinstance(self, Meta)))

class C(metaclass=Meta):
    pass

C.foo()

Dove,

  • la classe C è un'istanza della classe Meta
  • "class C" è un oggetto class che è un'istanza di "class Meta"
  • Come qualsiasi altro oggetto (istanza) "classe C" ha accesso ai suoi attributi / metodi definiti nella sua classe "classe Meta"
  • Quindi, decodifica "C.foo ()". "C" è l'istanza di "Meta" e "pippo" è il metodo che chiama attraverso l'istanza di "Meta" che è "C".
  • Il primo argomento del metodo "pippo" è riferimento all'istanza non alla classe a differenza di "classmethod"

Possiamo verificare come se "classe C" sia un'istanza di "Classe Meta

  isinstance(C, Meta)

Che cos'è il metodo di classe?

Si dice che i metodi Python siano associati. Poiché python impone la limitazione, questo metodo deve essere invocato solo con istanza. A volte potremmo voler invocare metodi direttamente attraverso la classe senza alcuna istanza (molto simile ai membri statici in Java) senza dover creare alcuna istanza. Per chiamare il metodo è necessaria l'istanza predefinita. Come soluzione alternativa, Python fornisce il metodo di classe di funzione integrata per associare il metodo dato alla classe anziché all'istanza.

Poiché i metodi di classe sono associati alla classe. Prende almeno un argomento che fa riferimento alla classe stessa anziché all'istanza (self)

se viene utilizzato il metodo di classe funzione / decoratore incorporato. Il primo argomento sarà riferimento alla classe anziché all'istanza

class ClassMethodDemo:
    @classmethod
    def foo(cls):
        print(f'cls is ClassMethodDemo: {cls is ClassMethodDemo}')

Poiché abbiamo usato "classmethod", chiamiamo il metodo "pippo" senza creare alcuna istanza come segue

ClassMethodDemo.foo()

La chiamata al metodo sopra restituirà True. Poiché il primo argomento cls è effettivamente riferimento a "ClassMethodDemo"

Sommario:

  • Il primo argomento di Classmethod è "un riferimento alla classe (tradizionalmente chiamata cls) stessa"
  • I metodi delle meta-classi non sono metodi di classe. I metodi delle Meta-classi ricevono il primo argomento che è "un riferimento all'istanza (tradizionalmente definita auto) non alla classe"
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.