lernen - python tutorial english




Was sind Metaklassen in Python? (10)

Was sind Metaklassen und wofür verwenden wir sie?

https://code.i-harness.com


Was sind Metaklassen? Wofür benutzt du sie?

TLDR: Eine Metaklasse instanziiert und definiert das Verhalten für eine Klasse wie eine Klasse, die instanziiert wird, und definiert das Verhalten für eine Instanz.

Pseudocode:

>>> Class(...)
instance

Das oben genannte sollte bekannt aussehen. Nun, wo kommt die Class ? Es ist eine Instanz einer Metaklasse (auch Pseudocode):

>>> Metaclass(...)
Class

In echtem Code können wir die Standard-Metaklasse, type , alles übergeben, was wir zum Instanziieren einer Klasse benötigen, und wir erhalten eine Klasse:

>>> type('Foo', (object,), {}) # requires a name, bases, and a namespace
<class '__main__.Foo'>

Anders ausgedrückt

  • Eine Klasse ist eine Instanz, wie eine Metaklasse eine Klasse.

    Wenn wir ein Objekt instanziieren, erhalten wir eine Instanz:

    >>> object()                          # instantiation of class
    <object object at 0x7f9069b4e0b0>     # instance
    

    Wenn wir eine Klasse explizit mit der Standard-Metaklasse type , wird sie instanziiert:

    >>> type('Object', (object,), {})     # instantiation of metaclass
    <class '__main__.Object'>             # instance
    
  • Anders ausgedrückt: Eine Klasse ist eine Instanz einer Metaklasse:

    >>> isinstance(object, type)
    True
    
  • Ein dritter Weg: Eine Metaklasse ist die Klasse einer Klasse.

    >>> type(object) == type
    True
    >>> object.__class__
    <class 'type'>
    

Wenn Sie eine Klassendefinition schreiben und diese von Python ausführen, verwendet sie eine Metaklasse, um das Klassenobjekt zu instanziieren (das wiederum dazu verwendet wird, Instanzen dieser Klasse zu instanziieren).

Genauso wie Klassendefinitionen verwendet werden können, um das Verhalten von benutzerdefinierten Objektinstanzen zu ändern, können wir eine Metaklassenklassendefinition verwenden, um das Verhalten eines Klassenobjekts zu ändern.

Wofür können sie verwendet werden? Aus den docs :

Die möglichen Verwendungen von Metaklassen sind grenzenlos. Einige Ideen, die erforscht wurden, umfassen Protokollierung, Schnittstellenprüfung, automatische Delegierung, automatische Erstellung von Eigenschaften, Proxies, Frameworks und automatische Sperrung / Synchronisierung von Ressourcen.

Nichtsdestotrotz wird der Benutzer in der Regel dazu angehalten, die Verwendung von Metaklassen zu vermeiden, sofern dies nicht unbedingt erforderlich ist.

Sie verwenden jedes Mal eine Metaklasse, wenn Sie eine Klasse erstellen:

Wenn Sie zum Beispiel eine Klassendefinition schreiben,

class Foo(object): 
    'demo'

Sie instanziieren ein Klassenobjekt.

>>> Foo
<class '__main__.Foo'>
>>> isinstance(Foo, type), isinstance(Foo, object)
(True, True)

Es entspricht dem funktional aufrufenden type mit den entsprechenden Argumenten und der Zuordnung des Ergebnisses zu einer Variablen dieses Namens:

name = 'Foo'
bases = (object,)
namespace = {'__doc__': 'demo'}
Foo = type(name, bases, namespace)

Beachten Sie, dass einige Dinge automatisch zum __dict__ hinzugefügt werden, dh zum Namespace:

>>> Foo.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Foo' objects>, 
'__module__': '__main__', '__weakref__': <attribute '__weakref__' 
of 'Foo' objects>, '__doc__': 'demo'})

Die Metaklasse des von uns erstellten Objekts ist in beiden Fällen type .

(Eine Randnotiz zum Inhalt der Klasse __dict__ : __module__ ist da, weil Klassen wissen müssen, wo sie definiert sind, und __dict__ und __weakref__ sind da, weil wir __slots__ nicht definieren - wenn wir __slots__ definieren, __slots__ wir ein bisschen Platz in den Instanzen, da wir __dict__ und __weakref__ durch Ausschluss von ihnen verbieten können, zum Beispiel:

>>> Baz = type('Bar', (object,), {'__doc__': 'demo', '__slots__': ()})
>>> Baz.__dict__
mappingproxy({'__doc__': 'demo', '__slots__': (), '__module__': '__main__'})

... Aber ich schweife ab.)

Wir können type wie jede andere Klassendefinition erweitern:

Hier ist die Standardeinstellung von Klassen:

>>> Foo
<class '__main__.Foo'>

Eines der wertvollsten Dinge, die wir standardmäßig beim Schreiben eines Python-Objekts tun können, ist, es mit einem guten __repr__ . Wenn wir help(repr) aufrufen help(repr) erfahren wir, dass es einen guten Test für __repr__ , der auch einen Test auf Gleichheit erfordert - obj == eval(repr(obj)) . Die folgende einfache Implementierung von __repr__ und __eq__ für Klasseninstanzen unseres Typs class bietet eine Demonstration, die die Standardwerte von __repr__ von Klassen verbessern kann:

class Type(type):
    def __repr__(cls):
        """
        >>> Baz
        Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
        >>> eval(repr(Baz))
        Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
        """
        metaname = type(cls).__name__
        name = cls.__name__
        parents = ', '.join(b.__name__ for b in cls.__bases__)
        if parents:
            parents += ','
        namespace = ', '.join(': '.join(
          (repr(k), repr(v) if not isinstance(v, type) else v.__name__))
               for k, v in cls.__dict__.items())
        return '{0}(\'{1}\', ({2}), {{{3}}})'.format(metaname, name, parents, namespace)
    def __eq__(cls, other):
        """
        >>> Baz == eval(repr(Baz))
        True            
        """
        return (cls.__name__, cls.__bases__, cls.__dict__) == (
                other.__name__, other.__bases__, other.__dict__)

Wenn wir nun ein Objekt mit dieser Metaklasse erstellen, bietet das __repr__Echo in der Befehlszeile einen viel weniger hässlichen Anblick als der Standard:

>>> class Bar(object): pass
>>> Baz = Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
>>> Baz
Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})

Mit einem __repr__für die Klasseninstanz definierten Code haben wir eine stärkere Möglichkeit, unseren Code zu debuggen. Eine weitere Überprüfung eval(repr(Class))ist jedoch unwahrscheinlich (da Funktionen normalerweise nicht von ihren Standardwerten abgewälzt werden könnten __repr__).

Eine erwartete Verwendung: __prepare__ein Namespace

Wenn wir beispielsweise wissen möchten, in welcher Reihenfolge die Methoden einer Klasse erstellt werden, können wir ein geordnetes Diktat als Namespace der Klasse angeben. Wir würden dies tun, mit __prepare__dem das Namespace dict für die Klasse zurückgegeben wird, wenn es in Python 3 implementiert ist :

from collections import OrderedDict

class OrderedType(Type):
    @classmethod
    def __prepare__(metacls, name, bases, **kwargs):
        return OrderedDict()
    def __new__(cls, name, bases, namespace, **kwargs):
        result = Type.__new__(cls, name, bases, dict(namespace))
        result.members = tuple(namespace)
        return result

Und Verwendung:

class OrderedMethodsObject(object, metaclass=OrderedType):
    def method1(self): pass
    def method2(self): pass
    def method3(self): pass
    def method4(self): pass

Und jetzt haben wir eine Aufzeichnung der Reihenfolge, in der diese Methoden (und andere Klassenattribute) erstellt wurden:

>>> OrderedMethodsObject.members
('__module__', '__qualname__', 'method1', 'method2', 'method3', 'method4')

Beachten Sie, dass dieses Beispiel aus der docs angepasst wurde - die neue Enumeration in der Standardbibliothek macht dies.

Also haben wir eine Metaklasse durch Erstellen einer Klasse instanziiert. Wir können die Metaklasse auch wie jede andere Klasse behandeln. Es hat eine Methodenauflösungsreihenfolge:

>>> inspect.getmro(OrderedType)
(<class '__main__.OrderedType'>, <class '__main__.Type'>, <class 'type'>, <class 'object'>)

Und es hat ungefähr das Richtige repr(was wir nicht mehr auswerten können, wenn wir keinen Weg finden, unsere Funktionen darzustellen).

>>> OrderedMethodsObject
OrderedType('OrderedMethodsObject', (object,), {'method1': <function OrderedMethodsObject.method1 at 0x0000000002DB01E0>, 'members': ('__module__', '__qualname__', 'method1', 'method2', 'method3', 'method4'), 'method3': <function OrderedMet
hodsObject.method3 at 0x0000000002DB02F0>, 'method2': <function OrderedMethodsObject.method2 at 0x0000000002DB0268>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'OrderedMethodsObject' objects>, '__doc__': None, '__d
ict__': <attribute '__dict__' of 'OrderedMethodsObject' objects>, 'method4': <function OrderedMethodsObject.method4 at 0x0000000002DB0378>})

Klassen als Objekte

Bevor Sie Metaklassen verstehen, müssen Sie Klassen in Python beherrschen. Und Python hat eine sehr spezielle Vorstellung davon, was Klassen sind, die der Smalltalk-Sprache entlehnt sind.

In den meisten Sprachen sind Klassen nur Codeteile, die beschreiben, wie ein Objekt erzeugt wird. Das stimmt auch in Python:

>>> class ObjectCreator(object):
...       pass
...

>>> my_object = ObjectCreator()
>>> print(my_object)
<__main__.ObjectCreator object at 0x8974f2c>

Aber Klassen sind mehr als das in Python. Klassen sind auch Objekte.

Ja, Gegenstände.

Sobald Sie die Schlüsselwortklasse verwenden, führt Python sie aus und erstellt ein OBJECT. Die Anleitung

>>> class ObjectCreator(object):
...       pass
...

erstellt im Speicher ein Objekt mit dem Namen "ObjectCreator".

Dieses Objekt (die Klasse) ist selbst in der Lage, Objekte (die Instanzen) zu erstellen, und deshalb ist es eine Klasse .

Trotzdem ist es ein Objekt und daher:

  • Sie können es einer Variablen zuweisen
  • Sie können es kopieren
  • Sie können Attribute hinzufügen
  • Sie können es als Funktionsparameter übergeben

z.B:

>>> print(ObjectCreator) # you can print a class because it's an object
<class '__main__.ObjectCreator'>
>>> def echo(o):
...       print(o)
...
>>> echo(ObjectCreator) # you can pass a class as a parameter
<class '__main__.ObjectCreator'>
>>> print(hasattr(ObjectCreator, 'new_attribute'))
False
>>> ObjectCreator.new_attribute = 'foo' # you can add attributes to a class
>>> print(hasattr(ObjectCreator, 'new_attribute'))
True
>>> print(ObjectCreator.new_attribute)
foo
>>> ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable
>>> print(ObjectCreatorMirror.new_attribute)
foo
>>> print(ObjectCreatorMirror())
<__main__.ObjectCreator object at 0x8997b4c>

Klassen dynamisch erstellen

Da Klassen Objekte sind, können Sie sie wie jedes Objekt im Handumdrehen erstellen.

Zunächst können Sie eine Klasse in einer Funktion mit class erstellen:

>>> def choose_class(name):
...     if name == 'foo':
...         class Foo(object):
...             pass
...         return Foo # return the class, not an instance
...     else:
...         class Bar(object):
...             pass
...         return Bar
...
>>> MyClass = choose_class('foo')
>>> print(MyClass) # the function returns a class, not an instance
<class '__main__.Foo'>
>>> print(MyClass()) # you can create an object from this class
<__main__.Foo object at 0x89c6d4c>

Aber es ist nicht so dynamisch, da Sie die ganze Klasse noch selbst schreiben müssen.

Da Klassen Objekte sind, müssen sie von etwas generiert werden.

Wenn Sie das Schlüsselwort class , erstellt Python dieses Objekt automatisch. Aber wie bei den meisten Dingen in Python können Sie dies auch manuell tun.

Erinnern Sie sich an den Funktionstyp? Die gute alte Funktion, mit der Sie wissen können, welchen Typ ein Objekt hat:

>>> print(type(1))
<type 'int'>
>>> print(type("1"))
<type 'str'>
>>> print(type(ObjectCreator))
<type 'type'>
>>> print(type(ObjectCreator()))
<class '__main__.ObjectCreator'>

Nun, type hat eine völlig andere Fähigkeit, es können auch Klassen on the fly erstellt werden. type kann die Beschreibung einer Klasse als Parameter annehmen und eine Klasse zurückgeben.

(Ich weiß, es ist dumm, dass dieselbe Funktion zwei unterschiedliche Verwendungszwecke haben kann, abhängig von den Parametern, die Sie an sie übergeben. Dies ist ein Problem aufgrund der Rückwärtskompatibilität in Python.)

type funktioniert so:

type(name of the class,
     tuple of the parent class (for inheritance, can be empty),
     dictionary containing attributes names and values)

z.B:

>>> class MyShinyClass(object):
...       pass

kann auf diese Weise manuell erstellt werden:

>>> MyShinyClass = type('MyShinyClass', (), {}) # returns a class object
>>> print(MyShinyClass)
<class '__main__.MyShinyClass'>
>>> print(MyShinyClass()) # create an instance with the class
<__main__.MyShinyClass object at 0x8997cec>

Sie werden feststellen, dass wir "MyShinyClass" als Namen der Klasse und als Variable für den Klassenverweis verwenden. Sie können unterschiedlich sein, aber es gibt keinen Grund, die Dinge zu komplizieren.

type akzeptiert ein Wörterbuch, um die Attribute der Klasse zu definieren. So:

>>> class Foo(object):
...       bar = True

Kann übersetzt werden in:

>>> Foo = type('Foo', (), {'bar':True})

Und als normale Klasse verwendet:

>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo.bar)
True
>>> f = Foo()
>>> print(f)
<__main__.Foo object at 0x8a9b84c>
>>> print(f.bar)
True

Und natürlich können Sie davon erben, also:

>>>   class FooChild(Foo):
...         pass

wäre:

>>> FooChild = type('FooChild', (Foo,), {})
>>> print(FooChild)
<class '__main__.FooChild'>
>>> print(FooChild.bar) # bar is inherited from Foo
True

Eventuell möchten Sie Ihrer Klasse Methoden hinzufügen. Definieren Sie einfach eine Funktion mit der richtigen Signatur und weisen Sie sie als Attribut zu.

>>> def echo_bar(self):
...       print(self.bar)
...
>>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')
False
>>> hasattr(FooChild, 'echo_bar')
True
>>> my_foo = FooChild()
>>> my_foo.echo_bar()
True

Sie können noch mehr Methoden hinzufügen, nachdem Sie die Klasse dynamisch erstellt haben, genau wie das Hinzufügen von Methoden zu einem normalerweise erstellten Klassenobjekt.

>>> def echo_bar_more(self):
...       print('yet another method')
...
>>> FooChild.echo_bar_more = echo_bar_more
>>> hasattr(FooChild, 'echo_bar_more')
True

Sie sehen, wohin wir gehen: In Python sind Klassen Objekte, und Sie können dynamisch eine Klasse erstellen.

Dies ist, was Python macht, wenn Sie die Schlüsselwortklasse verwenden, und dies mithilfe einer Metaklasse.

Was sind Metaklassen (endlich)

Metaklassen sind das Zeug, das Klassen erstellt.

Sie definieren Klassen, um Objekte zu erstellen, oder?

Wir haben jedoch gelernt, dass Python-Klassen Objekte sind.

Nun, Metaklassen erzeugen diese Objekte. Sie sind die Klassen der Klassen, die Sie sich so vorstellen können:

MyClass = MetaClass()
my_object = MyClass()

Sie haben gesehen, dass Sie mit diesem type so etwas tun können:

MyClass = type('MyClass', (), {})

Dies liegt daran, dass der Funktionstyp tatsächlich eine Metaklasse ist. type ist die Metaklasse, mit der Python alle Klassen hinter den Kulissen erstellt.

Jetzt wundern Sie sich, warum zum Teufel es in Kleinbuchstaben geschrieben wird und nicht Type ?

Nun, ich denke, es ist eine Frage der Konsistenz mit str , der Klasse, die Strings-Objekte erstellt, und int der Klasse, die Ganzzahl-Objekte erstellt. type ist nur die Klasse, die Klassenobjekte erstellt.

Sie sehen das, wenn Sie das Attribut __class__ überprüfen.

Alles und ich meine alles ist ein Gegenstand in Python. Dazu gehören Ints, Strings, Funktionen und Klassen. Alle sind Objekte. Und alle wurden aus einer Klasse erstellt:

>>> age = 35
>>> age.__class__
<type 'int'>
>>> name = 'bob'
>>> name.__class__
<type 'str'>
>>> def foo(): pass
>>> foo.__class__
<type 'function'>
>>> class Bar(object): pass
>>> b = Bar()
>>> b.__class__
<class '__main__.Bar'>

Was ist nun die __class__ von einer __class__ ?

>>> age.__class__.__class__
<type 'type'>
>>> name.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>

Eine Metaklasse ist also genau das, was Klassenobjekte erstellt.

Sie können es eine "Klassenfabrik" nennen, wenn Sie möchten.

type ist die eingebaute Metaklasse, die Python verwendet, Sie können jedoch selbstverständlich Ihre eigene Metaklasse erstellen.

Das __metaclass__ Attribut

In Python 2 können Sie ein __metaclass__ Attribut hinzufügen, wenn Sie eine Klasse schreiben (siehe nächster Abschnitt zur Python 3-Syntax):

class Foo(object):
    __metaclass__ = something...
    [...]

Wenn Sie dies tun, verwendet Python die Metaklasse, um die Klasse Foo zu erstellen.

Vorsicht, es ist knifflig.

Sie schreiben zuerst die class Foo(object) , aber das Klassenobjekt Foo ist noch nicht im Speicher erstellt.

Python __metaclass__ in der Klassendefinition nach __metaclass__ . Wenn es es findet, erstellt es die Objektklasse Foo . Ist dies nicht der Fall, wird die Klasse mit type erstellt.

Lesen Sie das mehrmals.

Wenn Sie das tun:

class Foo(Bar):
    pass

Python macht Folgendes:

Gibt es in Foo ein __metaclass__ Attribut?

Wenn ja, erstellen Sie im Speicher ein Klassenobjekt (ich sagte ein Klassenobjekt, bleiben Sie hier bei mir) mit dem Namen Foo indem Sie verwenden, was in __metaclass__ .

Wenn Python __metaclass__ nicht finden kann, __metaclass__ es auf MODULE-Ebene nach __metaclass__ und versucht das Gleiche zu tun (jedoch nur für Klassen, die nichts erben, im Grunde alte Klassen).

Wenn es dann keine __metaclass__ finden kann, verwendet es die eigene Metaklasse der Bar (das erste übergeordnete __metaclass__ ) (die möglicherweise der Standardtyp ist), um das Klassenobjekt zu erstellen.

__metaclass__ Sie hierbei darauf, dass das __metaclass__ Attribut nicht vererbt wird, sondern die Metaklasse des übergeordneten Bar.__class__ ( Bar.__class__ ). Wenn Bar ein __metaclass__ Attribut verwendet hat, das Bar mit type() (und nicht type.__new__() ) erstellt hat, erben die Unterklassen dieses Verhalten nicht.

Nun ist die große Frage, was können Sie in __metaclass__ ?

Die Antwort ist: etwas, das eine Klasse schaffen kann.

Und was kann eine Klasse schaffen? type oder alles, was Unterklassen oder verwendet.

Metaklassen in Python 3

Die Syntax zum Festlegen der Metaklasse wurde in Python 3 geändert:

class Foo(object, metaclass=something):
    [...]

dh das __metaclass__ Attribut wird nicht mehr verwendet, sondern für ein Schlüsselwortargument in der Liste der Basisklassen.

Das Verhalten von Metaklassen bleibt jedoch weitgehend gleich .

Benutzerdefinierte Metaklassen

Der Hauptzweck einer Metaklasse besteht darin, die Klasse automatisch zu ändern, wenn sie erstellt wird.

Normalerweise machen Sie dies für APIs, bei denen Sie Klassen erstellen möchten, die dem aktuellen Kontext entsprechen.

Stellen Sie sich ein dummes Beispiel vor, bei dem Sie entscheiden, dass für alle Klassen in Ihrem Modul die Attribute in Großbuchstaben geschrieben werden sollen. Es gibt mehrere Möglichkeiten, dies zu tun, aber eine Möglichkeit besteht darin, __metaclass__ auf Modulebene __metaclass__ .

Auf diese Weise werden alle Klassen dieses Moduls mit dieser Metaklasse erstellt, und wir müssen der Metaklasse lediglich mitteilen, dass alle Attribute in Großbuchstaben umgewandelt werden sollen.

Zum Glück kann __metaclass__ tatsächlich aufrufbar sein, es muss keine formale Klasse sein (ich weiß, etwas mit 'class' im Namen muss keine Klasse sein, go figure ... aber es ist hilfreich).

Wir beginnen mit einem einfachen Beispiel, indem wir eine Funktion verwenden.

# the metaclass will automatically get passed the same argument
# that you usually pass to `type`
def upper_attr(future_class_name, future_class_parents, future_class_attr):
    """
      Return a class object, with the list of its attribute turned
      into uppercase.
    """

    # pick up any attribute that doesn't start with '__' and uppercase it
    uppercase_attr = {}
    for name, val in future_class_attr.items():
        if not name.startswith('__'):
            uppercase_attr[name.upper()] = val
        else:
            uppercase_attr[name] = val

    # let `type` do the class creation
    return type(future_class_name, future_class_parents, uppercase_attr)

__metaclass__ = upper_attr # this will affect all classes in the module

class Foo(): # global __metaclass__ won't work with "object" though
    # but we can define __metaclass__ here instead to affect only this class
    # and this will work with "object" children
    bar = 'bip'

print(hasattr(Foo, 'bar'))
# Out: False
print(hasattr(Foo, 'BAR'))
# Out: True

f = Foo()
print(f.BAR)
# Out: 'bip'

Jetzt machen wir genau dasselbe, verwenden aber eine echte Klasse für eine Metaklasse:

# remember that `type` is actually a class like `str` and `int`
# so you can inherit from it
class UpperAttrMetaclass(type):
    # __new__ is the method called before __init__
    # it's the method that creates the object and returns it
    # while __init__ just initializes the object passed as parameter
    # you rarely use __new__, except when you want to control how the object
    # is created.
    # here the created object is the class, and we want to customize it
    # so we override __new__
    # you can do some stuff in __init__ too if you wish
    # some advanced use involves overriding __call__ as well, but we won't
    # see this
    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type(future_class_name, future_class_parents, uppercase_attr)

Das ist aber nicht wirklich OOP. Wir rufen type direkt an und überschreiben oder nennen das übergeordnete __new__ nicht __new__ . Machen wir das:

class UpperAttrMetaclass(type):

    def __new__(upperattr_metaclass, future_class_name,
                future_class_parents, future_class_attr):

        uppercase_attr = {}
        for name, val in future_class_attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        # reuse the type.__new__ method
        # this is basic OOP, nothing magic in there
        return type.__new__(upperattr_metaclass, future_class_name,
                            future_class_parents, uppercase_attr)

Möglicherweise haben Sie das zusätzliche Argument upperattr_metaclass bemerkt. Es ist nichts Besonderes daran: __new__ erhält immer die Klasse, in der sie definiert ist, als ersten Parameter. Genau wie Sie self für gewöhnliche Methoden, die die Instanz als ersten Parameter oder die definierende Klasse für Klassenmethoden erhalten.

Natürlich sind die Namen, die ich hier verwendet habe, aus Gründen der Klarheit lang, aber wie für sich self haben alle Argumente herkömmliche Namen. Eine echte Produktionsmetaklasse würde also so aussehen:

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return type.__new__(cls, clsname, bases, uppercase_attr)

Wir können es mit super noch sauberer machen, was die Vererbung erleichtert (da ja, Sie können Metaklassen haben, von Metaklassen erben, vom Typ erben):

class UpperAttrMetaclass(type):

    def __new__(cls, clsname, bases, dct):

        uppercase_attr = {}
        for name, val in dct.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val

        return super(UpperAttrMetaclass, cls).__new__(cls, clsname, bases, uppercase_attr)

Das ist es. Es gibt wirklich nichts mehr über Metaklassen.

Der Grund für die Komplexität des Codes bei der Verwendung von Metaklassen ist nicht auf Metaklassen zurückzuführen, sondern darauf, dass Sie in der Regel Metaklassen verwenden, um verdrehte __dict__ zu tun, die auf Introspektion beruhen, die Vererbung, vars wie __dict__ usw. beeinflussen.

In der Tat sind Metaklassen besonders nützlich, um schwarze Magie und daher komplizierte Dinge auszuführen. Aber an sich sind sie einfach:

  • eine Klassenerstellung abfangen
  • Ändern Sie die Klasse
  • Die modifizierte Klasse zurückgeben

Warum sollten Sie Metaklassen-Klassen anstelle von Funktionen verwenden?

Da __metaclass__ akzeptieren kann, warum sollten Sie eine Klasse verwenden, da diese offensichtlich komplizierter ist?

Dafür gibt es mehrere Gründe:

  • Die Absicht ist klar. Wenn Sie UpperAttrMetaclass(type) lesen, wissen Sie, was folgen wird
  • Sie können OOP verwenden. Metaklassen können von Metaklassen erben und übergeordnete Methoden überschreiben. Metaklassen können sogar Metaklassen verwenden.
  • Unterklassen einer Klasse sind Instanzen ihrer Metaklasse, wenn Sie eine Metaklassenklasse angegeben haben, jedoch nicht mit einer Metaklassenfunktion.
  • Sie können Ihren Code besser strukturieren. Sie verwenden Metaklassen niemals für etwas so Triviales wie im obigen Beispiel. Normalerweise ist es etwas kompliziertes. Die Möglichkeit, mehrere Methoden zu erstellen und in einer Klasse zu gruppieren, ist sehr hilfreich, um den Code leichter lesbar zu machen.
  • Sie können sich an __new__ , __init__ und __call__ . Damit können Sie verschiedene Sachen machen. Selbst wenn Sie normalerweise alles in __new__ , ist es für manche Leute einfach angenehmer, __init__ .
  • Das nennt man Metaklassen, verdammt noch mal! Es muss etwas bedeuten!

Warum verwenden Sie Metaklassen?

Nun die große Frage. Warum sollten Sie eine obskure, fehleranfällige Funktion verwenden?

Normalerweise tut man das nicht:

Metaklassen sind tiefere Magie, um die sich 99% der Benutzer nie sorgen sollten. Wenn Sie sich fragen, ob Sie sie brauchen, tun Sie das nicht (die Leute, die sie wirklich brauchen, wissen mit Sicherheit, dass sie sie brauchen und brauchen keine Erklärung dafür, warum).

Python Guru Tim Peters

Der Hauptanwendungsfall für eine Metaklasse ist das Erstellen einer API. Ein typisches Beispiel dafür ist der Django ORM.

Sie können so etwas definieren:

class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()

Aber wenn du das tust:

guy = Person(name='bob', age='35')
print(guy.age)

Es wird kein IntegerField Objekt zurückgegeben. Es wird ein int und kann sogar direkt aus der Datenbank übernommen werden.

Dies ist möglich, weil models.Model definiert und mit etwas Magie die Person Sie gerade mit einfachen Anweisungen definiert haben, in einen komplexen Hook zu einem Datenbankfeld verwandelt.

Django macht etwas komplexes Aussehen einfach, indem eine einfache API verfügbar gemacht wird und Metaklassen verwendet werden. Dabei wird Code von dieser API neu erstellt, um die eigentliche Arbeit hinter den Kulissen zu erledigen.

Das letzte Wort

Zunächst wissen Sie, dass Klassen Objekte sind, die Instanzen erstellen können.

Tatsächlich sind Klassen selbst Instanzen. Von metaklassen.

>>> class Foo(object): pass
>>> id(Foo)
142630324

Alles ist ein Objekt in Python und sie sind entweder Instanzen von Klassen oder Instanzen von Metaklassen.

Außer für den type .

type ist eigentlich eine eigene Metaklasse. Dies ist nicht etwas, das Sie in reinem Python reproduzieren könnten. Dies geschieht, indem Sie auf der Implementierungsebene ein wenig schummeln.

Zweitens sind Metaklassen kompliziert. Möglicherweise möchten Sie sie nicht für sehr einfache Klassenänderungen verwenden. Sie können Klassen durch zwei verschiedene Techniken ändern:

In 99% der Fälle, in denen Sie die Klasse ändern müssen, können Sie diese besser verwenden.

Aber in 98% der Fälle brauchen Sie überhaupt keine Klassenänderung.


Die tl; dr-Version

Die type(obj)Funktion liefert Ihnen den Typ eines Objekts.

Die type()Klasse einer Klasse ist ihre Metaklasse .

So verwenden Sie eine Metaklasse:

class Foo(object):
    __metaclass__ = MyMetaClass

Andere haben erklärt, wie Metaklassen funktionieren und wie sie in das Python-Typensystem passen. Hier ist ein Beispiel, wofür sie verwendet werden können. In einem Testrahmen, den ich geschrieben habe, wollte ich die Reihenfolge verfolgen, in der Klassen definiert wurden, damit ich sie später in dieser Reihenfolge instanziieren konnte. Ich fand es am einfachsten, dies mit einer Metaklasse zu tun.

class MyMeta(type):

    counter = 0

    def __init__(cls, name, bases, dic):
        type.__init__(cls, name, bases, dic)
        cls._order = MyMeta.counter
        MyMeta.counter += 1

class MyType(object):              # Python 2
    __metaclass__ = MyMeta

class MyType(metaclass=MyMeta):    # Python 3
    pass

Alles, was eine Unterklasse von MyType ist, erhält ein Klassenattribut _order , das die Reihenfolge aufzeichnet, in der die Klassen definiert wurden.


Eine Verwendung für Metaklassen besteht darin, einer Instanz automatisch neue Eigenschaften und Methoden hinzuzufügen.

Wenn Sie beispielsweise Django-Modelle betrachten , wirkt deren Definition etwas verwirrend. Es sieht so aus, als würden Sie nur Klasseneigenschaften definieren:

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

Zur Laufzeit werden die Person-Objekte jedoch mit allen möglichen nützlichen Methoden gefüllt. Sehen Sie sich die source für eine erstaunliche Metaklamerei an.


Ich denke, dass die ONLamp-Einführung in die Metaklassen-Programmierung gut geschrieben ist und eine sehr gute Einführung in das Thema gibt, obwohl sie bereits mehrere Jahre alt ist.

http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html (archiviert unter https://web.archive.org/web/20080206005253/http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html )

Kurz gesagt: Eine Klasse ist ein Bauplan für die Erstellung einer Instanz, eine Metaklasse ist ein Bauplan für die Erstellung einer Klasse. Es ist leicht zu erkennen, dass Klassen in Python auch erstklassige Objekte sein müssen, um dieses Verhalten zu ermöglichen.

Ich habe selbst noch nie eine geschrieben, aber ich denke, eine der schönsten Anwendungen von Metaklassen kann im Django-Framework gesehen werden . Die Modellklassen verwenden einen Metaklassen-Ansatz, um eine deklarative Schreibweise für neue Modelle oder Formularklassen zu ermöglichen. Während die Metaklasse die Klasse erstellt, haben alle Mitglieder die Möglichkeit, die Klasse selbst anzupassen.

Was Sie noch zu sagen haben: Wenn Sie nicht wissen, was Metaklassen sind, ist die Wahrscheinlichkeit, dass Sie sie nicht benötigen, 99%.


Beachten Sie, dass diese Antwort für Python 2.x gilt, da sie 2008 geschrieben wurde. Die Metaklassen unterscheiden sich in 3.x geringfügig, siehe die Kommentare.

Metaklassen sind die geheime Sauce, die Klasse zum Laufen bringt. Die Standard-Metaklasse für ein neues Stilobjekt wird als "Typ" bezeichnet.

class type(object)
  |  type(object) -> the object's type
  |  type(name, bases, dict) -> a new type

Metaklassen nehmen 3 Args. ' Name ', ' Basen ' und ' Dikt '

Hier beginnt das Geheimnis. Suchen Sie in dieser Beispielklassendefinition nach, woher der Name, die Basis und das Diktat stammen.

class ThisIsTheName(Bases, Are, Here):
    All_the_code_here
    def doesIs(create, a):
        dict

Definieren Sie eine Metaklasse, die zeigt, wie ' class: ' sie aufruft.

def test_metaclass(name, bases, dict):
    print 'The Class Name is', name
    print 'The Class Bases are', bases
    print 'The dict has', len(dict), 'elems, the keys are', dict.keys()

    return "yellow"

class TestName(object, None, int, 1):
    __metaclass__ = test_metaclass
    foo = 1
    def baz(self, arr):
        pass

print 'TestName = ', repr(TestName)

# output => 
The Class Name is TestName
The Class Bases are (<type 'object'>, None, <type 'int'>, 1)
The dict has 4 elems, the keys are ['baz', '__module__', 'foo', '__metaclass__']
TestName =  'yellow'

Und jetzt, ein Beispiel, das tatsächlich etwas bedeutet, werden die Variablen in der Liste "Attribute" automatisch für die Klasse festgelegt und auf Keine gesetzt.

def init_attributes(name, bases, dict):
    if 'attributes' in dict:
        for attr in dict['attributes']:
            dict[attr] = None

    return type(name, bases, dict)

class Initialised(object):
    __metaclass__ = init_attributes
    attributes = ['foo', 'bar', 'baz']

print 'foo =>', Initialised.foo
# output=>
foo => None

Beachten Sie, dass das magische Verhalten, das "Initalized" durch die Metaklasse "init_attributes" erzielt, nicht an eine Unterklasse von "Initalized" übergeben wird.

Hier ist ein noch konkreteres Beispiel, das zeigt, wie Sie eine Unterklasse 'type' erstellen können, um eine Metaklasse zu erstellen, die eine Aktion ausführt, wenn die Klasse erstellt wird. Das ist ziemlich knifflig:

class MetaSingleton(type):
    instance = None
    def __call__(cls, *args, **kw):
        if cls.instance is None:
            cls.instance = super(MetaSingleton, cls).__call__(*args, **kw)
        return cls.instance

 class Foo(object):
     __metaclass__ = MetaSingleton

 a = Foo()
 b = Foo()
 assert a is b

Python 3-Update

Es gibt (zu diesem Zeitpunkt) zwei Schlüsselmethoden in einer Metaklasse:

  • __prepare__ , und
  • __new__

__prepare__können Sie eine benutzerdefinierte Zuordnung (z. B. eine OrderedDict) angeben, die während der Erstellung der Klasse als Namespace verwendet werden soll. Sie müssen eine Instanz des von Ihnen ausgewählten Namespaces zurückgeben. Wenn Sie kein __prepare__Normal implementieren, dictwird verwendet.

__new__ ist für die eigentliche Erstellung / Änderung der Abschlussklasse verantwortlich.

Eine bloße, nichts-zusätzliche Metaklasse möchte:

class Meta(type):

    def __prepare__(metaclass, cls, bases):
        return dict()

    def __new__(metacls, cls, bases, clsdict):
        return super().__new__(metacls, cls, bases, clsdict)

Ein einfaches Beispiel:

Angenommen, Sie möchten einen einfachen Validierungscode für Ihre Attribute ausführen - als müsste es immer ein intoder ein sein str. Ohne eine Metaklasse würde Ihre Klasse ungefähr so ​​aussehen:

class Person:
    weight = ValidateType('weight', int)
    age = ValidateType('age', int)
    name = ValidateType('name', str)

Wie Sie sehen, müssen Sie den Namen des Attributs zweimal wiederholen. Dies ermöglicht Tippfehler mit irritierenden Fehlern.

Eine einfache Metaklasse kann dieses Problem lösen:

class Person(metaclass=Validator):
    weight = ValidateType(int)
    age = ValidateType(int)
    name = ValidateType(str)

So würde die Metaklasse aussehen (wird nicht verwendet, __prepare__da sie nicht benötigt wird):

class Validator(type):
    def __new__(metacls, cls, bases, clsdict):
        # search clsdict looking for ValidateType descriptors
        for name, attr in clsdict.items():
            if isinstance(attr, ValidateType):
                attr.name = name
                attr.attr = '_' + name
        # create final class and return it
        return super().__new__(metacls, cls, bases, clsdict)

Ein Beispiellauf von:

p = Person()
p.weight = 9
print(p.weight)
p.weight = '9'

produziert:

9
Traceback (most recent call last):
  File "simple_meta.py", line 36, in <module>
    p.weight = '9'
  File "simple_meta.py", line 24, in __set__
    (self.name, self.type, value))
TypeError: weight must be of type(s) <class 'int'> (got '9')

Anmerkung : Dieses Beispiel ist einfach genug, es hätte auch mit einem Klassendekorateur durchgeführt werden können, aber vermutlich würde eine Metaklasse viel mehr leisten.

Die 'ValidateType'-Klasse als Referenz:

class ValidateType:
    def __init__(self, type):
        self.name = None  # will be set by metaclass
        self.attr = None  # will be set by metaclass
        self.type = type
    def __get__(self, inst, cls):
        if inst is None:
            return self
        else:
            return inst.__dict__[self.attr]
    def __set__(self, inst, value):
        if not isinstance(value, self.type):
            raise TypeError('%s must be of type(s) %s (got %r)' %
                    (self.name, self.type, value))
        else:
            inst.__dict__[self.attr] = value

Eine Metaklasse ist eine Klasse, die angibt, wie (einige) andere Klassen erstellt werden sollen.

In diesem Fall sah ich Metaclass als Lösung für mein Problem: Ich hatte ein wirklich kompliziertes Problem, das wahrscheinlich anders hätte gelöst werden können, aber ich entschied mich für eine Metaklasse. Aufgrund der Komplexität ist dies eines der wenigen Module, das ich geschrieben habe, bei dem die Kommentare im Modul die Menge an geschriebenem Code überschreiten. Hier ist es...

#!/usr/bin/env python

# Copyright (C) 2013-2014 Craig Phillips.  All rights reserved.

# This requires some explaining.  The point of this metaclass excercise is to
# create a static abstract class that is in one way or another, dormant until
# queried.  I experimented with creating a singlton on import, but that did
# not quite behave how I wanted it to.  See now here, we are creating a class
# called GsyncOptions, that on import, will do nothing except state that its
# class creator is GsyncOptionsType.  This means, docopt doesn't parse any
# of the help document, nor does it start processing command line options.
# So importing this module becomes really efficient.  The complicated bit
# comes from requiring the GsyncOptions class to be static.  By that, I mean
# any property on it, may or may not exist, since they are not statically
# defined; so I can't simply just define the class with a whole bunch of
# properties that are @property @staticmethods.
#
# So here's how it works:
#
# Executing 'from libgsync.options import GsyncOptions' does nothing more
# than load up this module, define the Type and the Class and import them
# into the callers namespace.  Simple.
#
# Invoking 'GsyncOptions.debug' for the first time, or any other property
# causes the __metaclass__ __getattr__ method to be called, since the class
# is not instantiated as a class instance yet.  The __getattr__ method on
# the type then initialises the class (GsyncOptions) via the __initialiseClass
# method.  This is the first and only time the class will actually have its
# dictionary statically populated.  The docopt module is invoked to parse the
# usage document and generate command line options from it.  These are then
# paired with their defaults and what's in sys.argv.  After all that, we
# setup some dynamic properties that could not be defined by their name in
# the usage, before everything is then transplanted onto the actual class
# object (or static class GsyncOptions).
#
# Another piece of magic, is to allow command line options to be set in
# in their native form and be translated into argparse style properties.
#
# Finally, the GsyncListOptions class is actually where the options are
# stored.  This only acts as a mechanism for storing options as lists, to
# allow aggregation of duplicate options or options that can be specified
# multiple times.  The __getattr__ call hides this by default, returning the
# last item in a property's list.  However, if the entire list is required,
# calling the 'list()' method on the GsyncOptions class, returns a reference
# to the GsyncListOptions class, which contains all of the same properties
# but as lists and without the duplication of having them as both lists and
# static singlton values.
#
# So this actually means that GsyncOptions is actually a static proxy class...
#
# ...And all this is neatly hidden within a closure for safe keeping.
def GetGsyncOptionsType():
    class GsyncListOptions(object):
        __initialised = False

    class GsyncOptionsType(type):
        def __initialiseClass(cls):
            if GsyncListOptions._GsyncListOptions__initialised: return

            from docopt import docopt
            from libgsync.options import doc
            from libgsync import __version__

            options = docopt(
                doc.__doc__ % __version__,
                version = __version__,
                options_first = True
            )

            paths = options.pop('<path>', None)
            setattr(cls, "destination_path", paths.pop() if paths else None)
            setattr(cls, "source_paths", paths)
            setattr(cls, "options", options)

            for k, v in options.iteritems():
                setattr(cls, k, v)

            GsyncListOptions._GsyncListOptions__initialised = True

        def list(cls):
            return GsyncListOptions

        def __getattr__(cls, name):
            cls.__initialiseClass()
            return getattr(GsyncListOptions, name)[-1]

        def __setattr__(cls, name, value):
            # Substitut option names: --an-option-name for an_option_name
            import re
            name = re.sub(r'^__', "", re.sub(r'-', "_", name))
            listvalue = []

            # Ensure value is converted to a list type for GsyncListOptions
            if isinstance(value, list):
                if value:
                    listvalue = [] + value
                else:
                    listvalue = [ None ]
            else:
                listvalue = [ value ]

            type.__setattr__(GsyncListOptions, name, listvalue)

    # Cleanup this module to prevent tinkering.
    import sys
    module = sys.modules[__name__]
    del module.__dict__['GetGsyncOptionsType']

    return GsyncOptionsType

# Our singlton abstract proxy class.
class GsyncOptions(object):
    __metaclass__ = GetGsyncOptionsType()

Python-Klassen sind selbst - wie in der Instanz - Objekte ihrer Meta-Klasse.

Die Standard-Metaklasse, die angewendet wird, wenn Sie Klassen wie folgt festlegen:

class foo:
    ...

Metaklassen werden verwendet, um einige Regeln auf eine ganze Gruppe von Klassen anzuwenden. Angenommen, Sie erstellen ein ORM, um auf eine Datenbank zuzugreifen, und Sie möchten, dass Datensätze aus jeder Tabelle einer Klasse zugeordnet sind, die dieser Tabelle zugeordnet ist (basierend auf Feldern, Geschäftsregeln usw.). Dies ist eine mögliche Verwendung der Metaklasse ist beispielsweise die Verbindungspoollogik, die von allen Datensatzklassen aus allen Tabellen gemeinsam genutzt wird. Eine andere Verwendung ist die Logik, um Fremdschlüssel zu unterstützen, die mehrere Klassen von Datensätzen umfassen.

Wenn Sie eine Metaklasse definieren, geben Sie den Unterklassentyp an und können die folgenden magischen Methoden überschreiben, um Ihre Logik einzufügen.

class somemeta(type):
    __new__(mcs, name, bases, clsdict):
      """
  mcs: is the base metaclass, in this case type.
  name: name of the new class, as provided by the user.
  bases: tuple of base classes 
  clsdict: a dictionary containing all methods and attributes defined on class

  you must return a class object by invoking the __new__ constructor on the base metaclass. 
 ie: 
    return type.__call__(mcs, name, bases, clsdict).

  in the following case:

  class foo(baseclass):
        __metaclass__ = somemeta

  an_attr = 12

  def bar(self):
      ...

  @classmethod
  def foo(cls):
      ...

      arguments would be : ( somemeta, "foo", (baseclass, baseofbase,..., object), {"an_attr":12, "bar": <function>, "foo": <bound class method>}

      you can modify any of these values before passing on to type
      """
      return type.__call__(mcs, name, bases, clsdict)


    def __init__(self, name, bases, clsdict):
      """ 
      called after type has been created. unlike in standard classes, __init__ method cannot modify the instance (cls) - and should be used for class validaton.
      """
      pass


    def __prepare__():
        """
        returns a dict or something that can be used as a namespace.
        the type will then attach methods and attributes from class definition to it.

        call order :

        somemeta.__new__ ->  type.__new__ -> type.__init__ -> somemeta.__init__ 
        """
        return dict()

    def mymethod(cls):
        """ works like a classmethod, but for class objects. Also, my method will not be visible to instances of cls.
        """
        pass

Jedenfalls sind diese beiden am häufigsten verwendeten Haken. Metaklassierung ist mächtig, und darüber hinaus gibt es bei weitem keine erschöpfende Liste von Verwendungen für Metaklassierung.





python-datamodel