Python-Metaklassen

Python-Metaklassen

Der Begriff Metaprogrammierung bezieht sich auf das Potenzial eines Programms, sich selbst zu kennen oder zu manipulieren. Python unterstützt eine Form der Metaprogrammierung für Klassen namens metaclasses .

Metaklassen sind ein esoterisches OOP-Konzept, das hinter praktisch dem gesamten Python-Code lauert. Sie verwenden sie, unabhängig davon, ob Sie sich dessen bewusst sind oder nicht. Zum größten Teil müssen Sie sich dessen nicht bewusst sein. Die meisten Python-Programmierer müssen selten, wenn überhaupt, an Metaklassen denken.

Bei Bedarf bietet Python jedoch eine Funktion, die nicht alle objektorientierten Sprachen unterstützen: Sie können unter die Haube gehen und benutzerdefinierte Metaklassen definieren. Die Verwendung von benutzerdefinierten Metaklassen ist etwas umstritten, wie aus dem folgenden Zitat von Tim Peters hervorgeht, dem Python-Guru, der die Zen of Python verfasst hat:

_ _ „Metaklassen sind eine tiefere Magie, als 99% der Benutzer sich jemals Sorgen machen sollten. Wenn Sie sich fragen, ob Sie sie brauchen, tun Sie das nicht (die Leute, die sie tatsächlich brauchen, wissen mit Sicherheit, dass sie sie brauchen, und brauchen keine Erklärung, warum). "

  • Tim Peters _ _

Es gibt Pythonisten (wie Python-Liebhaber genannt werden), die glauben, dass Sie niemals benutzerdefinierte Metaklassen verwenden sollten. Das mag ein bisschen weit gehen, aber es ist wahrscheinlich richtig, dass benutzerdefinierte Metaklassen meistens nicht erforderlich sind. Wenn es nicht offensichtlich ist, dass ein Problem sie erfordert, ist es wahrscheinlich sauberer und lesbarer, wenn es auf einfachere Weise gelöst wird.

Das Verständnis von Python-Metaklassen lohnt sich jedoch, da es zu einem besseren Verständnis der Interna von Python-Klassen im Allgemeinen führt. Sie wissen es nie: Vielleicht befinden Sie sich eines Tages in einer dieser Situationen, in denen Sie nur wissen, dass Sie eine benutzerdefinierte Metaklasse wünschen.

*Benachrichtigung erhalten:* Verpassen Sie nicht das Follow-up zu diesem Tutorial - https://realpython.com/bonus/newsletter-dont-miss-updates/[Klicken Sie hier, um am Real Python Newsletter teilzunehmen] und Sie werden wissen, wann Die nächste Rate kommt heraus.

Old-Style vs. Klassen neuen Stils

Im Python-Bereich kann eine Klasse https://wiki.python.org/moin/NewClassVsClassicClass[ eine von zwei Varianten sein. Es wurde keine offizielle Terminologie festgelegt, daher werden sie informell als Klassen im alten und neuen Stil bezeichnet.

Old-Style-Klassen

Bei Klassen alten Stils sind Klasse und Typ nicht ganz dasselbe. Eine Instanz einer Klasse alten Stils wird immer von einem einzelnen integrierten Typ namens "+ Instanz " implementiert. Wenn " obj " eine Instanz einer Klasse alten Stils ist, bezeichnet " obj . class " die Klasse, aber " type (obj) " ist immer " instance +". Das folgende Beispiel stammt aus Python 2.7:

>>>

>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x.__class__
<class __main__.Foo at 0x000000000535CC48>
>>> type(x)
<type 'instance'>

Klassen neuen Stils

Klassen neuen Stils vereinheitlichen die Konzepte von Klasse und Typ. Wenn "+ obj " eine Instanz einer Klasse neuen Stils ist, ist " type (obj) " dasselbe wie " obj . class +":

>>>

>>> class Foo:
...     pass
>>> obj = Foo()
>>> obj.__class__
<class '__main__.Foo'>
>>> type(obj)
<class '__main__.Foo'>
>>> obj.__class__ is type(obj)
True

>>>

>>> n = 5
>>> d = { 'x' : 1, 'y' : 2 }

>>> class Foo:
...     pass
...
>>> x = Foo()

>>> for obj in (n, d, x):
...     print(type(obj) is obj.__class__)
...
True
True
True

Typ und Klasse

In Python 3 sind alle Klassen Klassen neuen Stils. Daher ist es in Python 3 sinnvoll, den Objekttyp und seine Klasse austauschbar zu verwenden.

*Hinweis:* In Python 2 sind Klassen standardmäßig im alten Stil. Vor Python 2.2 wurden Klassen neuen Stils überhaupt nicht unterstützt. Ab Python 2.2 können sie erstellt werden, müssen jedoch explizit als neuer Stil deklariert werden.

Denken Sie daran, dass in Python unter alles ein Objekt ist. Klassen sind ebenfalls Objekte. Daher muss eine Klasse einen Typ haben. Was ist der Typ einer Klasse?

Folgendes berücksichtigen:

>>>

>>> class Foo:
...     pass
...
>>> x = Foo()

>>> type(x)
<class '__main__.Foo'>

>>> type(Foo)
<class 'type'>

Der Typ von "+ x " ist wie erwartet die Klasse " Foo ". Aber der Typ von " Foo ", die Klasse selbst, ist " Typ ". Im Allgemeinen ist der Typ einer Klasse neuen Stils " Typ +".

Der Typ der integrierten Klassen, mit denen Sie vertraut sind, ist ebenfalls + Typ +:

>>>

>>> for t in int, float, dict, list, tuple:
...     print(type(t))
...
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>

Im Übrigen ist der Typ von "+ Typ " auch " Typ +" (ja, wirklich):

>>>

>>> type(type)
<class 'type'>

+ type + ist eine Metaklasse, von der Klassen Instanzen sind. So wie ein gewöhnliches Objekt eine Instanz einer Klasse ist, ist jede Klasse neuen Stils in Python und damit jede Klasse in Python 3 eine Instanz der Metaklasse + type +.

Im obigen Fall:

  • + x + ist eine Instanz der Klasse + Foo +.

  • + Foo + ist eine Instanz der Metaklasse + Typ +.

  • + type + ist auch eine Instanz der Metaklasse + type +, also eine Instanz von sich.

Python-Klassenkette

Eine Klasse dynamisch definieren

Die integrierte Funktion + type () + gibt bei Übergabe eines Arguments den Typ eines Objekts zurück. Für Klassen neuen Stils entspricht dies im Allgemeinen dem https://docs.python.org/3/library/stdtypes.html#instance.class[objects Attribut + class +]:

>>>

>>> type(3)
<class 'int'>

>>> type(['foo', 'bar', 'baz'])
<class 'list'>

>>> t = (1, 2, 3, 4, 5)
>>> type(t)
<class 'tuple'>

>>> class Foo:
...     pass
...
>>> type(Foo())
<class '__main__.Foo'>

Sie können ` type () + `auch mit drei Argumenten aufrufen: + type (<name>, <bases>, <dct>) + `:

  • + <Name> + gibt den Klassennamen an. Dies wird zum Attribut + name + der Klasse.

  • + <bases> + gibt ein Tupel der Basisklassen an, von denen die Klasse erbt. Dies wird zum Attribut + Basen + der Klasse.

  • + <dct> + gibt ein Namespace-Wörterbuch an, das Definitionen für den Klassenkörper enthält. Dies wird zum Attribut + dict + der Klasse.

Wenn Sie auf diese Weise + type () + aufrufen, wird eine neue Instanz der Metaklasse + type + erstellt. Mit anderen Worten, es wird dynamisch eine neue Klasse erstellt.

In jedem der folgenden Beispiele definiert das oberste Snippet eine Klasse dynamisch mit + type () +, während das Snippet darunter die Klasse auf die übliche Weise mit der Anweisung + class + definiert. In jedem Fall sind die beiden Schnipsel funktional äquivalent.

Beispiel 1

In diesem ersten Beispiel sind die an + type () + übergebenen Argumente + <bases> + und + <dct> + beide leer. Es wird keine Vererbung von einer übergeordneten Klasse angegeben, und zunächst wird nichts in das Namespace-Wörterbuch eingefügt. Dies ist die einfachste mögliche Klassendefinition:

>>>

>>> Foo = type('Foo', (), {})

>>> x = Foo()
>>> x
<__main__.Foo object at 0x04CFAD50>

>>>

>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x
<__main__.Foo object at 0x0370AD50>

Beispiel 2

Hier ist "+ <Basis> " ein Tupel mit einem einzelnen Element " Foo ", das die übergeordnete Klasse angibt, von der " Bar " erbt. Das Attribut " attr +" wird zunächst in das Namespace-Wörterbuch eingefügt:

>>>

>>> Bar = type('Bar', (Foo,), dict(attr=100))

>>> x = Bar()
>>> x.attr
100
>>> x.__class__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)

>>>

>>> class Bar(Foo):
...     attr = 100
...

>>> x = Bar()
>>> x.attr
100
>>> x.__class__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)

Beispiel 3

Dieses Mal ist + <bases> + wieder leer. Zwei Objekte werden über das Argument + <dct> + in das Namespace-Wörterbuch eingefügt. Das erste ist ein Attribut mit dem Namen "+ attr " und das zweite eine Funktion mit dem Namen " attr_val +", die zu einer Methode der definierten Klasse wird:

>>>

>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': lambda x : x.attr
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100

>>>

>>> class Foo:
...     attr = 100
...     def attr_val(self):
...         return self.attr
...

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100

Beispiel 4

Mit https://dbader.org/blog/python-lambda-functions [+ lambda + in Python] können nur sehr einfache Funktionen definiert werden. Im folgenden Beispiel wird eine etwas komplexere Funktion extern definiert und dann im Namespace-Wörterbuch über den Namen + f + + attr_val + zugewiesen:

>>>

>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': f
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
attr = 100

>>>

>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> class Foo:
...     attr = 100
...     attr_val = f
...

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
attr = 100

Benutzerdefinierte Metaklassen

Betrachten Sie noch einmal dieses abgenutzte Beispiel:

>>>

>>> class Foo:
...     pass
...
>>> f = Foo()

Der Ausdruck "+ Foo () " erstellt eine neue Instanz der Klasse " Foo ". Wenn der Interpreter auf ` Foo () +` trifft, geschieht Folgendes:

  • Die Methode + call () + der übergeordneten Klasse von + Foo + wird aufgerufen. Da + Foo + eine Standardklasse im neuen Stil ist, ist ihre übergeordnete Klasse die Metaklasse + type +, daher wird die Methode + + call __ () + von + type + `aufgerufen.

  • Diese + call () + Methode ruft wiederum Folgendes auf:

  • + new () +

  • + init () +

Wenn "+ Foo " nicht " new () " und " init () " definiert, werden Standardmethoden von den Vorfahren von " Foo " geerbt. Wenn jedoch " Foo " diese Methoden definiert, überschreiben sie diejenigen aus der Abstammung, was ein benutzerdefiniertes Verhalten beim Instanziieren von " Foo +" ermöglicht.

Im Folgenden wird eine benutzerdefinierte Methode mit dem Namen "+ new () " definiert und als " new () " -Methode für " Foo +" zugewiesen:

>>>

>>> def new(cls):
...     x = object.__new__(cls)
...     x.attr = 100
...     return x
...
>>> Foo.__new__ = new

>>> f = Foo()
>>> f.attr
100

>>> g = Foo()
>>> g.attr
100

Dies ändert das Instanziierungsverhalten der Klasse "+ Foo ": Jedes Mal, wenn eine Instanz von " Foo " erstellt wird, wird sie standardmäßig mit einem Attribut namens " attr " initialisiert, das den Wert " 100 " hat. (Code wie dieser wird normalerweise in der Methode ` init () ` und normalerweise nicht in ` new () +` angezeigt. Dieses Beispiel dient zu Demonstrationszwecken.)

Wie bereits wiederholt, sind Klassen nun auch Objekte. Angenommen, Sie möchten das Instanziierungsverhalten beim Erstellen einer Klasse wie "+ Foo " auf ähnliche Weise anpassen. Wenn Sie dem obigen Muster folgen, definieren Sie erneut eine benutzerdefinierte Methode und weisen sie als " new () " -Methode für die Klasse zu, deren Instanz " Foo " ist. ` Foo ` ist eine Instanz der Metaklasse ` type +`, daher sieht der Code ungefähr so ​​aus:

>>>

# Spoiler alert:  This doesn't work!
>>> def new(cls):
...     x = type.__new__(cls)
...     x.attr = 100
...     return x
...
>>> type.__new__ = new
Traceback (most recent call last):
  File "<pyshell#77>", line 1, in <module>
    type.__new__ = new
TypeError: can't set attributes of built-in/extension type 'type'

Wie Sie sehen, können Sie die Methode "+ new () " der Metaklasse " type +" nicht neu zuweisen. Python erlaubt es nicht.

Das ist wahrscheinlich genauso gut. + type + ist die Metaklasse, von der alle Klassen neuen Stils abgeleitet werden. Du solltest sowieso nicht damit herumspielen. Aber welchen Rückgriff gibt es dann, wenn Sie die Instanziierung einer Klasse anpassen möchten?

Eine mögliche Lösung ist eine benutzerdefinierte Metaklasse. Anstatt mit der Metaklasse "+ Typ " herumzuspielen, können Sie im Wesentlichen Ihre eigene Metaklasse definieren, die sich von " Typ +" ableitet, und stattdessen können Sie damit herumspielen.

Der erste Schritt besteht darin, eine Metaklasse, die von "+ Typ +" abgeleitet ist, wie folgt zu definieren:

>>>

>>> class Meta(type):
...     def __new__(cls, name, bases, dct):
...         x = super().__new__(cls, name, bases, dct)
...         x.attr = 100
...         return x
...

Der Definitionsheader "+ Klasse Meta (Typ): " gibt an, dass " Meta " von " Typ " abgeleitet ist. Da " type " eine Metaklasse ist, ist " Meta +" auch eine Metaklasse.

Beachten Sie, dass für + Meta + eine benutzerdefinierte + new () + Methode definiert wurde. Es war nicht möglich, dies direkt mit der Metaklasse "+ Typ " zu tun. Die Methode ` new () +` führt Folgendes aus:

  • Delegiert über + super () + an die + new () + Methode der übergeordneten Metaklasse (+ type +), um tatsächlich eine neue Klasse zu erstellen

  • Weist der Klasse das benutzerdefinierte Attribut "+ attr " mit dem Wert " 100 +" zu

  • Gibt die neu erstellte Klasse zurück

Jetzt die andere Hälfte des Voodoos: Definieren Sie eine neue Klasse "+ Foo " und geben Sie an, dass ihre Metaklasse die benutzerdefinierte Metaklasse " Meta " ist und nicht die Standard-Metaklasse " Typ ". Dies erfolgt mit dem Schlüsselwort ` metaclass +` in der Klassendefinition wie folgt:

>>>

>>> class Foo(metaclass=Meta):
...     pass
...
>>> Foo.attr
100

_Voila! _ + Foo + hat das Attribut + attr + automatisch aus der Metaklasse + Meta + übernommen. Natürlich tun alle anderen Klassen, die Sie ähnlich definieren, dasselbe:

>>>

>>> class Bar(metaclass=Meta):
...     pass
...
>>> class Qux(metaclass=Meta):
...     pass
...
>>> Bar.attr, Qux.attr
(100, 100)

So wie eine Klasse als Vorlage für die Erstellung von Objekten fungiert, fungiert eine Metaklasse als Vorlage für die Erstellung von Klassen. Metaklassen werden manchmal als class factories bezeichnet.

Vergleichen Sie die folgenden zwei Beispiele:

*Objektfabrik:*

>>>

>>> class Foo:
...     def __init__(self):
...         self.attr = 100
...

>>> x = Foo()
>>> x.attr
100

>>> y = Foo()
>>> y.attr
100

>>> z = Foo()
>>> z.attr
100
*Klassenfabrik:*

>>>

>>> class Meta(type):
...     def __init__(
...         cls, name, bases, dct
...     ):
...         cls.attr = 100
...
>>> class X(metaclass=Meta):
...     pass
...
>>> X.attr
100

>>> class Y(metaclass=Meta):
...     pass
...
>>> Y.attr
100

>>> class Z(metaclass=Meta):
...     pass
...
>>> Z.attr
100

Ist das wirklich notwendig?

So einfach das obige Beispiel einer Klassenfabrik ist, es ist die Essenz der Funktionsweise von Metaklassen. Sie ermöglichen die Anpassung der Klasseninstanziierung.

Trotzdem ist dies eine Menge Aufwand, nur um jeder neu erstellten Klasse das benutzerdefinierte Attribut "+ attr +" zu verleihen. Benötigen Sie wirklich eine Metaklasse dafür?

In Python gibt es mindestens ein paar andere Möglichkeiten, wie effektiv dasselbe erreicht werden kann:

*Einfache Vererbung:*

>>>

>>> class Base:
...     attr = 100
...

>>> class X(Base):
...     pass
...

>>> class Y(Base):
...     pass
...

>>> class Z(Base):
...     pass
...

>>> X.attr
100
>>> Y.attr
100
>>> Z.attr
100
*Klassendekorateur:*

>>>

>>> def decorator(cls):
...     class NewClass(cls):
...         attr = 100
...     return NewClass
...
>>> @decorator
... class X:
...     pass
...
>>> @decorator
... class Y:
...     pass
...
>>> @decorator
... class Z:
...     pass
...

>>> X.attr
100
>>> Y.attr
100
>>> Z.attr
100

Fazit

Wie Tim Peters vorschlägt, können Metaklassen leicht in den Bereich einer „Lösung auf der Suche nach einem Problem“ übergehen. Es ist normalerweise nicht erforderlich, benutzerdefinierte Metaklassen zu erstellen. Wenn das vorliegende Problem auf einfachere Weise gelöst werden kann, sollte es wahrscheinlich so sein. Dennoch ist es vorteilhaft, Metaklassen zu verstehen, damit Sie Python classes im Allgemeinen verstehen und erkennen können, wann eine Metaklasse wirklich das geeignete Werkzeug ist.