Was sind die Unterschiede zwischen einer Klassenmethode und einer Metaklassenmethode?

12

In Python kann ich mit dem @classmethodDecorator eine Klassenmethode erstellen :

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

Alternativ kann ich eine normale (Instanz-) Methode für eine Metaklasse verwenden:

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

Wie die Ausgabe von zeigt C.f(), bieten diese beiden Ansätze eine ähnliche Funktionalität.

Was sind die Unterschiede zwischen der Verwendung @classmethodund der Verwendung einer normalen Methode für eine Metaklasse?

user200783
quelle

Antworten:

5

Da Klassen Instanzen einer Metaklasse sind, ist es nicht unerwartet, dass sich eine "Instanzmethode" in der Metaklasse wie eine Klassenmethode verhält.

Ja, es gibt jedoch Unterschiede - und einige davon sind mehr als semantisch:

  1. Der wichtigste Unterschied besteht darin , dass ein Verfahren in der Metaklasse ist nicht „sichtbar“ aus einer Klasse Instanz . Dies liegt daran, dass die Attributsuche in Python (vereinfacht - Deskriptoren haben möglicherweise Vorrang) nach einem Attribut in der Instanz sucht. Wenn es in der Instanz nicht vorhanden ist, sucht Python in der Klasse dieser Instanz und die Suche wird fortgesetzt die Oberklassen der Klasse, aber nicht die Klassen der Klasse. Die Python-stdlib verwendet diese Funktion in der abc.ABCMeta.registerMethode. Diese Funktion kann für immer verwendet werden, da Methoden, die sich auf die Klasse selbst beziehen, ohne Konflikte als Instanzattribute wiederverwendet werden können (eine Methode würde jedoch immer noch Konflikte verursachen).
  2. Ein weiterer Unterschied, obwohl offensichtlich ist, dass ein Verfahren erklärt , in der Metaklasse kann in mehreren Klassen zur Verfügung steht, soweit nicht anderweitig in Verbindung stehend - wenn Sie verschiedene Klassenhierarchien haben, nicht verwandt in was sie behandeln, wollen aber einige gemeinsame Funktionalität für alle Klassen Sie müssten eine Mixin-Klasse erstellen, die als Basis in beiden Hierarchien enthalten sein müsste (z. B. um alle Klassen in eine Anwendungsregistrierung aufzunehmen). (NB. Das Mixin ist manchmal ein besserer Anruf als eine Metaklasse.)
  3. Eine Klassenmethode ist ein spezialisiertes "Klassenmethoden" -Objekt, während eine Methode in der Metaklasse eine gewöhnliche Funktion ist.

Es kommt also vor, dass der Mechanismus, den Klassenmethoden verwenden, der " Deskriptorprotokoll " ist. Während normale Funktionen eine __get__Methode enthalten, die das selfArgument einfügt, wenn sie aus einer Instanz abgerufen werden, und dieses Argument leer lässt, wenn sie aus einer Klasse abgerufen werden, hat ein classmethodObjekt ein anderes __get__, das die Klasse selbst (den "Eigentümer") als das einfügt erster Parameter in beiden Situationen.

Dies macht die meiste Zeit keine praktischen Unterschiede. Wenn Sie jedoch auf die Methode als Funktion zugreifen möchten, um ihr dynamisch einen Dekorator hinzuzufügen, oder eine andere, für eine Methode in der Metaklasse, wird meta.methoddie Funktion abgerufen, die zur Verwendung bereit ist , während Sie es verwenden müssen cls.my_classmethod.__func__ , um es aus einer Klassenmethode abzurufen (und dann müssen Sie ein anderes classmethodObjekt erstellen und es zurückweisen, wenn Sie etwas umbrechen).

Grundsätzlich sind dies die 2 Beispiele:


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)

Mit anderen Worten: Abgesehen von dem wichtigen Unterschied, dass eine in der Metaklasse definierte Methode von der Instanz aus sichtbar ist und ein classmethodObjekt nicht, erscheinen die anderen Unterschiede zur Laufzeit dunkel und bedeutungslos - aber das geschieht, weil die Sprache nicht gehen muss mit speziellen Regeln für Klassenmethoden aus dem Weg: Beide Arten der Deklaration einer Klassenmethode sind als Folge des Sprachdesigns möglich - eine für die Tatsache, dass eine Klasse selbst ein Objekt ist, und eine andere als eine Möglichkeit unter vielen von die Verwendung des Deskriptorprotokolls, mit dem der Attributzugriff in einer Instanz und in einer Klasse spezialisiert werden kann:

Das classmethodeingebaute System ist in nativem Code definiert, könnte jedoch nur in reinem Python codiert werden und würde genauso funktionieren. Der Balg der 5-Zeilen-Klasse kann als classmethodDekorateur verwendet werden, ohne dass Laufzeitunterschiede zum eingebauten @classmethod" at all (though distinguishable through introspection such as calls toisinstance- , and evenRepräsentanten bestehen.


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

Über die Methoden hinaus ist es interessant zu bedenken, dass spezialisierte Attribute wie ein @propertyauf der Metaklasse trotzdem als spezialisierte Klassenattribute funktionieren, ohne dass ein überraschendes Verhalten vorliegt.

jsbueno
quelle
2

Wenn Sie es so formulieren, wie Sie es in der Frage getan haben, wird das @classmethod und die Metaklassen möglicherweise ähnlich aus, haben aber ziemlich unterschiedliche Zwecke. Die Klasse, die in das @classmethodArgument von 'eingefügt wird, wird normalerweise zum Erstellen einer Instanz verwendet (dh eines alternativen Konstruktors). Andererseits werden die Metaklassen normalerweise verwendet, um die Klasse selbst zu ändern (z. B. wie Django es mit seinen DSL-Modellen macht).

Das heißt nicht, dass Sie die Klasse innerhalb einer Klassenmethode nicht ändern können. Aber dann stellt sich die Frage, warum Sie die Klasse nicht so definiert haben, wie Sie sie überhaupt ändern möchten. Wenn nicht, schlägt es möglicherweise einen Refactor vor, mehrere Klassen zu verwenden.

Lassen Sie uns das erste Beispiel etwas erweitern.

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

In Anlehnung an die Python-Dokumente wird das Obige auf Folgendes erweitert:

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)

Beachten Sie, wie __get__entweder eine Instanz oder die Klasse (oder beide) verwendet werden kann, und somit können Sie beide C.fund ausführen C().f. Dies ist anders als das von Ihnen angegebene Metaklassen-Beispiel, das ein AttributeErrorfor auslöstC().f .

Darüber hinaus existiert im Metaklassenbeispiel fnicht in C.__dict__. Wenn der Interpreter das Attribut fmit C.fnachschlägt, schaut er nach C.__dict__und dann, nachdem er es nicht gefunden hat, nach type(C).__dict__(was ist M.__dict__). Dies kann wichtig , wenn Sie die Flexibilität außer Kraft setzen möchten fin C, obwohl ich das bezweifeln immer von praktischem Nutzen sein wird.

Kent Shikama
quelle
0

In Ihrem Beispiel liegt der Unterschied in einigen anderen Klassen, für deren Metaklasse M festgelegt ist.

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'

Hier erfahren Sie mehr über Metaklassen Was sind einige (konkrete) Anwendungsfälle für Metaklassen?

andole
quelle
0

Sowohl @classmethod als auch Metaclass sind unterschiedlich.

Alles in Python ist ein Objekt. Alles bedeutet alles.

Was ist Metaclass?

Wie gesagt, alles ist ein Objekt. Klassen sind auch Objekte. Tatsächlich sind Klassen Instanzen anderer mysteriöser Objekte, die formal als Metaklassen bezeichnet werden. Die Standard-Metaklasse in Python ist "Typ", wenn nicht angegeben

Standardmäßig sind alle definierten Klassen Instanzen vom Typ.

Klassen sind Instanzen von Meta-Klassen

Nur wenige wichtige Punkte sind das Verständnis des Verhaltens

  • As-Klassen sind Instanzen von Metaklassen.
  • Wie jedes instanziierte Objekt erhalten auch Objekte (Instanzen) ihre Attribute von der Klasse. Die Klasse erhält ihre Attribute von der Meta-Klasse

Betrachten Sie den folgenden Code

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

Wo,

  • Klasse C ist eine Instanz der Klasse Meta
  • "Klasse C" ist ein Klassenobjekt, das eine Instanz von "Klasse Meta" ist.
  • Wie jedes andere Objekt (Instanz) hat "Klasse C" Zugriff auf seine Attribute / Methoden, die in ihrer Klasse "Klasse Meta" definiert sind.
  • Also "C.foo ()" dekodieren. "C" ist eine Instanz von "Meta" und "foo" ist ein Methodenaufruf durch die Instanz von "Meta", was "C" ist.
  • Das erste Argument der Methode "foo" bezieht sich auf eine Instanz, die im Gegensatz zu "classmethod" keine Klasse ist.

Wir können überprüfen, ob "Klasse C" eine Instanz von "Class Meta" ist

  isinstance(C, Meta)

Was ist Klassenmethode?

Python-Methoden sollen gebunden sein. Da Python die Einschränkung auferlegt, muss diese Methode nur mit der Instanz aufgerufen werden. Manchmal möchten wir Methoden direkt über die Klasse ohne Instanz aufrufen (ähnlich wie statische Mitglieder in Java), ohne eine Instanz erstellen zu müssen. Zum Aufrufen der Methode ist eine Standardinstanz erforderlich. Als Problemumgehung bietet Python eine integrierte Funktion classmethod, um die angegebene Methode anstelle der Instanz an die Klasse zu binden.

Als Klasse sind Methoden an die Klasse gebunden. Es wird mindestens ein Argument verwendet, das sich auf die Klasse selbst anstelle der Instanz (self) bezieht.

wenn die integrierte Funktion / Dekorator-Klassenmethode verwendet wird. Das erste Argument bezieht sich auf die Klasse anstelle der Instanz

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

Da wir "classmethod" verwendet haben, rufen wir die Methode "foo" auf, ohne eine Instanz wie folgt zu erstellen

ClassMethodDemo.foo()

Der obige Methodenaufruf gibt True zurück. Da das erste Argument cls tatsächlich auf "ClassMethodDemo" verweist

Zusammenfassung:

  • Classmethods erhalten das erste Argument, das "eine Referenz auf die Klasse (traditionell als cls bezeichnet) selbst ist".
  • Methoden von Metaklassen sind keine Klassenmethoden. Methoden von Meta-Klassen erhalten das erste Argument, das "ein Verweis auf eine Instanz (traditionell als Selbst bezeichnet) ist, nicht auf eine Klasse".
Neotam
quelle