Was sind Metaklassen? Wofür verwenden Sie sie?
TLDR: Eine Metaklasse instanziiert und definiert das Verhalten einer Klasse, genauso wie eine Klasse das Verhalten einer Instanz instanziiert und definiert.
Pseudocode:
>>> Class(...)
instance
Das oben Gesagte sollte Ihnen bekannt vorkommen. Nun, woher kommt Class
herkommen? Es ist eine Instanz einer Metaklasse (auch Pseudocode):
>>> Metaclass(...)
Class
In echtem Code können wir die Standard-Metaklasse übergeben, type
alles, was wir brauchen, um eine Klasse zu instanziieren, und wir erhalten eine Klasse:
>>> type('Foo', (object,), {}) # requires a name, bases, and a namespace
<class '__main__.Foo'>
Anders formuliert
-
Eine Klasse ist für eine Instanz das, was eine Metaklasse für eine Klasse ist.
Wenn wir ein Objekt instanziieren, erhalten wir eine Instanz:
>>> object() # instantiation of class
<object object at 0x7f9069b4e0b0> # instance
Gleiches gilt, wenn wir eine Klasse explizit mit der Standard-Metaklasse definieren, type
instanziieren wir es:
>>> type('Object', (object,), {}) # instantiation of metaclass
<class '__main__.Object'> # instance
-
Anders ausgedrückt: Eine Klasse ist eine Instanz einer Metaklasse:
>>> isinstance(object, type)
True
-
Oder anders ausgedrückt: Eine Metaklasse ist die Klasse einer Klasse.
>>> type(object) == type
True
>>> object.__class__
<class 'type'>
Wenn Sie eine Klassendefinition schreiben und Python sie ausführt, verwendet es eine Metaklasse, um das Klassenobjekt zu instanziieren (das wiederum verwendet wird, um Instanzen dieser Klasse zu instanziieren).
Genauso wie wir Klassendefinitionen verwenden können, um das Verhalten von benutzerdefinierten Objektinstanzen zu ändern, können wir eine Metaklassendefinition verwenden, um das Verhalten eines Klassenobjekts zu ändern.
Wofür können sie verwendet werden? Von der docs :
Die Einsatzmöglichkeiten von Metaklassen sind grenzenlos. Einige Ideen, die untersucht wurden, umfassen Protokollierung, Schnittstellenprüfung, automatische Delegation, automatische Eigenschaftserstellung, Proxies, Frameworks und automatische Ressourcensperrung/Synchronisierung.
Dennoch wird den Benutzern in der Regel empfohlen, die Verwendung von Metaklassen zu vermeiden, sofern dies nicht unbedingt erforderlich ist.
Sie verwenden eine Metaklasse jedes Mal, wenn Sie eine Klasse erstellen:
Wenn Sie eine Klassendefinition schreiben, zum Beispiel wie diese,
class Foo(object):
'demo'
Sie instanziieren ein Klassenobjekt.
>>> Foo
<class '__main__.Foo'>
>>> isinstance(Foo, type), isinstance(Foo, object)
(True, True)
Es ist dasselbe wie der funktionale Aufruf von type
mit den entsprechenden Argumenten und weist das Ergebnis einer Variablen mit diesem Namen zu:
name = 'Foo'
bases = (object,)
namespace = {'__doc__': 'demo'}
Foo = type(name, bases, namespace)
Beachten Sie, dass einige Dinge automatisch in die __dict__
d.h. der Namensraum:
>>> Foo.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Foo' objects>,
'__module__': '__main__', '__weakref__': <attribute '__weakref__'
of 'Foo' objects>, '__doc__': 'demo'})
Le site Metaklasse des von uns erstellten Objekts ist in beiden Fällen type
.
(Eine Randbemerkung zu den Inhalten der Klasse __dict__
: __module__
ist da, weil Klassen wissen müssen, wo sie definiert sind, und __dict__
y __weakref__
sind da, weil wir nicht definieren __slots__
- wenn wir definieren. __slots__
sparen wir ein wenig Platz in den Instanzen, da wir die Möglichkeit haben, die __dict__
y __weakref__
indem man sie ausschließt. Zum Beispiel:
>>> Baz = type('Bar', (object,), {'__doc__': 'demo', '__slots__': ()})
>>> Baz.__dict__
mappingproxy({'__doc__': 'demo', '__slots__': (), '__module__': '__main__'})
... aber ich schweife ab.)
Wir können erweitern type
genau wie jede andere Klassendefinition:
Hier ist die Standardeinstellung __repr__
von Klassen:
>>> Foo
<class '__main__.Foo'>
Eines der wertvollsten Dinge, die wir beim Schreiben eines Python-Objekts standardmäßig tun können, ist, es mit einer guten __repr__
. Wenn wir anrufen help(repr)
erfahren wir, dass es einen guten Test für eine __repr__
die auch einen Test auf Gleichheit erfordert - obj == eval(repr(obj))
. Die folgende einfache Implementierung von __repr__
y __eq__
für Klasseninstanzen unserer Typklasse bietet uns eine Demonstration, die den Standard verbessern kann __repr__
von Klassen:
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, wird die __repr__
in der Befehlszeile bietet einen viel weniger hässlichen Anblick als die Standardeinstellung:
>>> class Bar(object): pass
>>> Baz = Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
>>> Baz
Type('Baz', (Foo, Bar,), {'__module__': '__main__', '__doc__': None})
Mit einem schönen __repr__
für die Klasseninstanz definiert ist, haben wir eine bessere Möglichkeit, unseren Code zu debuggen. Allerdings ist eine weitergehende Überprüfung mit eval(repr(Class))
ist unwahrscheinlich (da es ziemlich unmöglich wäre, Funktionen von ihrem Standardwert zu evalieren __repr__
's).
Eine erwartete Verwendung: __prepare__
einen Namensraum
Wenn wir zum Beispiel wissen wollen, in welcher Reihenfolge die Methoden einer Klasse erstellt werden, können wir ein geordnetes Diktat als Namensraum der Klasse angeben. Wir würden dies tun mit __prepare__
die gibt das Namespace-Dict für die Klasse zurück, wenn sie 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')
Anmerkung: Dieses Beispiel wurde aus dem Dokumentation - die neue enum in der Standardbibliothek tut dies.
Wir haben also eine Metaklasse instanziiert, indem wir eine Klasse erstellt haben. Wir können die Metaklasse auch wie jede andere Klasse behandeln. Sie hat eine Reihenfolge der Methodenauflösung:
>>> inspect.getmro(OrderedType)
(<class '__main__.OrderedType'>, <class '__main__.Type'>, <class 'type'>, <class 'object'>)
Und es hat ungefähr die richtige repr
(die wir nicht mehr auswerten können, es sei denn, wir finden einen Weg, 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>})