Viele Leute denken, dass Metaprogrammierung in Python den Code unnötig kompliziert, aber wenn Sie ihn richtig verwenden, können Sie komplexe Entwurfsmuster schnell und elegant implementieren. Darüber hinaus verwenden bekannte Python-Frameworks wie Django, DRF und SQLAlchemy Metaklassen, um eine einfache Erweiterbarkeit und eine einfache Wiederverwendung von Code zu ermöglichen.

In diesem Artikel werde ich Ihnen erklären, warum Sie keine Angst haben sollten, Metaprogrammierung in Ihren Projekten zu verwenden, und zeigen, für welche Aufgaben es am besten ist. Weitere Informationen zu Metaprogrammierungsoptionen finden Sie im Advanced Python- Kurs.
Erinnern wir uns zunächst an die Grundlagen der Metaprogrammierung in Python. Es ist nicht überflüssig hinzuzufügen, dass alles, was unten geschrieben wird, für Python Version 3.5 und höher gilt.
Eine kurze Tour durch das Python-Datenmodell
Wir alle wissen also, dass alles in Python ein Objekt ist, und es ist kein Geheimnis, dass es für jedes Objekt eine bestimmte Klasse gibt, von der es generiert wurde, zum Beispiel:
>>> def f(): pass >>> type(f) <class 'function'>
Der Typ des Objekts oder der Klasse, mit der das Objekt generiert wurde, kann mithilfe der integrierten Typfunktion bestimmt werden, die eine ziemlich interessante Aufrufsignatur aufweist (wir werden etwas später darauf eingehen). Der gleiche Effekt kann erzielt werden, indem das Attribut __class__
für ein beliebiges Objekt abgeleitet wird.
Um Funktionen zu erstellen, wird eine bestimmte integrierte Klassenfunktion function
. Mal sehen, was wir damit machen können. Nehmen Sie dazu das Leerzeichen aus dem Modul für integrierte Typen :
>>> from types import FunctionType >>> FunctionType <class 'function'> >>> help(FunctionType) class function(object) | function(code, globals[, name[, argdefs[, closure]]]) | | Create a function object from a code object and a dictionary. | The optional name string overrides the name from the code object. | The optional argdefs tuple specifies the default argument values. | The optional closure tuple supplies the bindings for free variables.
Wie wir sehen können, ist jede Funktion in Python eine Instanz der oben beschriebenen Klasse. Versuchen wir nun, eine neue Funktion zu erstellen, ohne auf ihre Deklaration durch def
. Dazu müssen wir lernen, wie Codeobjekte mit der im Interpreter integrierten Kompilierungsfunktion erstellt werden:
Großartig! Mit Hilfe von Meta-Tools haben wir gelernt, wie man Funktionen im laufenden Betrieb erstellt, aber in der Praxis wird dieses Wissen selten verwendet. Schauen wir uns nun an, wie Klassenobjekte und Instanzobjekte dieser Klassen erstellt werden:
>>> class User: pass >>> user = User() >>> type(user) <class '__main__.User'> >>> type(User) <class 'type'>
Es ist offensichtlich, dass die User
zum Erstellen einer user
wird. Es ist viel interessanter, sich die type
anzusehen, mit der die User
selbst erstellt wird. Hier wenden wir uns der zweiten Option zum Aufrufen der integrierten Typfunktion zu, die in Kombination eine Metaklasse für jede Klasse in Python darstellt. Eine Metaklasse ist per Definition eine Klasse, deren Instanz eine andere Klasse ist. Mit Metaklassen können wir den Prozess zum Erstellen einer Klasse anpassen und den Prozess zum Erstellen einer Instanz einer Klasse teilweise steuern.
Gemäß der Dokumentation gibt der zweite Signaturtyp type(name, bases, attrs)
- einen neuen Datentyp oder, wenn auf einfache Weise - eine neue Klasse zurück, und das __name__
wird zum __name__
-Attribut der zurückgegebenen Klasse, bases
- die Liste der übergeordneten Klassen wird als __bases__
verfügbar __bases__
. Nun, attrs
- ein attrs
Objekt, das alle Attribute und Methoden der Klasse enthält, wird in __dict__
. Das Prinzip der Funktion kann in Python als einfacher Pseudocode beschrieben werden:
type(name, bases, attrs) ~ class name(bases): attrs
Mal sehen, wie Sie mit nur dem Typaufruf eine völlig neue Klasse erstellen können:
>>> User = type('User', (), {}) >>> User <class '__main__.User'>
Wie Sie sehen, müssen wir das Schlüsselwort class
nicht verwenden, um eine neue Klasse zu erstellen. Die Typfunktion verzichtet darauf. Schauen wir uns nun ein komplizierteres Beispiel an:
class User: def __init__(self, name): self.name = name class SuperUser(User): """Encapsulate domain logic to work with super users""" group_name = 'admin' @property def login(self): return f'{self.group_name}/{self.name}'.lower()
Wie Sie den obigen Beispielen entnehmen können, ist die Beschreibung von Klassen und Funktionen mit den Schlüsselwörtern class
und def
nur syntaktischer Zucker, und alle Arten von Objekten können durch gewöhnliche Aufrufe integrierter Funktionen erstellt werden. Lassen Sie uns abschließend darüber sprechen, wie Sie die dynamische Erstellung von Klassen in realen Projekten verwenden können.
Manchmal müssen wir Informationen vom Benutzer oder von anderen externen Quellen nach einem zuvor bekannten Datenschema validieren. Zum Beispiel möchten wir das Benutzeranmeldeformular im Admin-Bereich ändern - Felder löschen und hinzufügen, die Strategie ihrer Validierung ändern usw.
Versuchen wir zur Veranschaulichung, dynamisch ein Django- Formular zu erstellen, dessen Beschreibung im folgenden json
Format gespeichert ist:
{ "fist_name": { "type": "str", "max_length": 25 }, "last_name": { "type": "str", "max_length": 30 }, "age": { "type": "int", "min_value": 18, "max_value": 99 } }
Erstellen Sie nun basierend auf der obigen Beschreibung eine Reihe von Feldern und ein neues Formular mit der bereits bekannten Typfunktion:
import json from django import forms fields_type_map = { 'str': forms.CharField, 'int': forms.IntegerField, }
Großartig! Jetzt können Sie das erstellte Formular in die Vorlage übertragen und für den Benutzer rendern. Der gleiche Ansatz kann mit anderen Frameworks für die Datenvalidierung und -präsentation ( DRF-Serializer , Marshmallow und andere) verwendet werden.
Oben haben wir uns die bereits abgeschlossene Metaklasse angesehen, aber meistens erstellen Sie im Code Ihre eigenen Metaklassen und verwenden sie, um die Erstellung neuer Klassen und ihrer Instanzen zu konfigurieren. Im allgemeinen Fall sieht das "Leerzeichen" einer Metaklasse folgendermaßen aus:
class MetaClass(type): """ : mcs – , <__main__.MetaClass> name – , , , "User" bases – -, (SomeMixin, AbstractUser) attrs – dict-like , cls – , <__main__.User> extra_kwargs – keyword- args kwargs – """ def __new__(mcs, name, bases, attrs, **extra_kwargs): return super().__new__(mcs, name, bases, attrs) def __init__(cls, name, bases, attrs, **extra_kwargs): super().__init__(cls) @classmethod def __prepare__(mcs, cls, bases, **extra_kwargs): return super().__prepare__(mcs, cls, bases, **kwargs) def __call__(cls, *args, **kwargs): return super().__call__(*args, **kwargs)
Um diese Metaklasse zum Konfigurieren der User
, wird die folgende Syntax verwendet:
class User(metaclass=MetaClass): def __new__(cls, name): return super().__new__(cls) def __init__(self, name): self.name = name
Das Interessanteste ist die Reihenfolge, in der der Python-Interpreter die Metaklassen-Metamethode zum Zeitpunkt der Erstellung der Klasse selbst aufruft:
- Der Interpreter ermittelt und findet die übergeordneten Klassen für die aktuelle Klasse (falls vorhanden).
- Der Interpreter definiert eine Metaklasse (in unserem Fall
MetaClass
). - Die Methode
MetaClass.__prepare__
wird MetaClass.__prepare__
Sie sollte ein diktartiges Objekt zurückgeben, in das die Attribute und Methoden der Klasse geschrieben werden. Danach wird das Objekt über das Argument MetaClass.__new__
attrs
. Wir werden etwas später in den Beispielen über die praktische Anwendung dieser Methode sprechen. - Der Interpreter liest den Hauptteil der
User
Klasse und generiert Parameter, um sie an die MetaClass
Metaklasse zu übergeben. - Die
MetaClass.__new__
Methode MetaClass.__new__
- die MetaClass.__new__
Methode gibt das erstellte Klassenobjekt zurück. Wir haben die Argumente name
, attrs
und attrs
bereits getroffen, als wir sie an die type
Funktion übergeben haben, und wir werden etwas später über den Parameter **extra_kwargs
sprechen. Wenn der Typ des attrs
Arguments mit __prepare__
geändert wurde, muss es in ein __prepare__
konvertiert werden, bevor es an den Methodenaufruf super()
wird. - Die
MetaClass.__init__
Methode MetaClass.__init__
- die Initialisierungsmethode, mit der Sie dem Klassenobjekt in der Klasse zusätzliche Attribute und Methoden hinzufügen können. In der Praxis wird es in Fällen verwendet, in denen Metaklassen von anderen Metaklassen geerbt werden, andernfalls ist alles, was in __init__
getan werden kann, besser in __new__
. Beispielsweise kann der Parameter __slots__
nur in der Methode __new__
werden, indem er in das attrs
Objekt geschrieben wird. - In diesem Schritt wird die Klasse als erstellt betrachtet.
Erstellen Sie nun eine Instanz unserer User
und sehen Sie sich die Aufrufkette an:
user = User(name='Alyosha')
- Zum Zeitpunkt des Aufrufs von
User(...)
ruft User(...)
Interpreter die MetaClass.__call__(name='Alyosha')
-Methode auf, bei der das Klassenobjekt und die übergebenen Argumente übergeben werden. MetaClass.__call__
ruft User.__new__(name='Alyosha')
- eine Konstruktormethode, die eine Instanz der User
Klasse erstellt und zurückgibt- Als
MetaClass.__call__
ruft MetaClass.__call__
User.__init__(name='Alyosha')
- eine Initialisierungsmethode, die der erstellten Instanz neue Attribute hinzufügt. MetaClass.__call__
gibt die erstellte und initialisierte Instanz der User
Klasse zurück.- Zu diesem Zeitpunkt wird eine Instanz der Klasse als erstellt betrachtet.
Diese Beschreibung deckt natürlich nicht alle Nuancen der Verwendung von Metaklassen ab, aber es reicht aus, mit der Metaprogrammierung zu beginnen, um einige Architekturmuster zu implementieren. Vorwärts zu den Beispielen!
Abstrakte Klassen
Das allererste Beispiel finden Sie in der Standardbibliothek: ABCMeta - Mit einer Metaklasse können Sie jede unserer Klassen als abstrakt deklarieren und alle Nachkommen dazu zwingen, vordefinierte Methoden, Eigenschaften und Attribute zu implementieren.
from abc import ABCMeta, abstractmethod class BasePlugin(metaclass=ABCMeta): """ supported_formats run """ @property @abstractmethod def supported_formats(self) -> list: pass @abstractmethod def run(self, input_data: dict): pass
Wenn nicht alle abstrakten Methoden und Attribute im Erben implementiert sind, erhalten wir beim Versuch, eine Instanz der TypeError
zu erstellen, einen TypeError
:
class VideoPlugin(BasePlugin): def run(self): print('Processing video...') plugin = VideoPlugin()
Die Verwendung abstrakter Klassen hilft dabei, die Basisklassenschnittstelle sofort zu reparieren und zukünftige Vererbungsfehler zu vermeiden, z. B. Tippfehler im Namen einer überschriebenen Methode.
Automatisches Registrierungs-Plugin-System
Sehr oft wird die Metaprogrammierung verwendet, um verschiedene Entwurfsmuster zu implementieren. Fast jedes bekannte Framework verwendet Metaklassen, um Registrierungsobjekte zu erstellen. Solche Objekte speichern Links zu anderen Objekten und ermöglichen den schnellen Empfang an einer beliebigen Stelle im Programm. Betrachten Sie ein einfaches Beispiel für die automatische Registrierung von Plugins zum Abspielen von Mediendateien in verschiedenen Formaten.
Metaklassenimplementierung:
class RegistryMeta(ABCMeta): """ , . " " -> " " """ _registry_formats = {} def __new__(mcs, name, bases, attrs): cls: 'BasePlugin' = super().__new__(mcs, name, bases, attrs)
Und hier sind die Plugins selbst. Wir übernehmen die BasePlugin
Implementierung aus dem vorherigen Beispiel:
class BasePlugin(metaclass=RegistryMeta): ... class VideoPlugin(BasePlugin): supported_formats = ['mpg', 'mov'] def run(self): ... class AudioPlugin(BasePlugin): supported_formats = ['mp3', 'flac'] def run(self): ...
Nach dem Ausführen dieses Codes registriert der Interpreter 4 Formate und 2 Plugins in unserer Registrierung, die diese Formate verarbeiten können:
>>> RegistryMeta.show_registry() {'flac': <class '__main__.AudioPlugin'>, 'mov': <class '__main__.VideoPlugin'>, 'mp3': <class '__main__.AudioPlugin'>, 'mpg': <class '__main__.VideoPlugin'>} >>> plugin_class = RegistryMeta.get_plugin('mov') >>> plugin_class <class '__main__.VideoPlugin'> >>> plugin_class().run() Processing video...
Es ist erwähnenswert, dass eine weitere interessante Nuance der Arbeit mit Metaklassen vorhanden ist. Dank der nicht offensichtlichen Reihenfolge der Methodenauflösung können wir die show_registry
Methode nicht nur für die RegistyMeta
Klasse RegistyMeta
, sondern für jede andere Klasse, von der es sich um eine Metaklasse handelt:
>>> AudioPlugin.get_plugin('avi')
Mithilfe von Metaklassen können Sie Klassenattributnamen als Metadaten für andere Objekte verwenden. Nichts ist klar? Aber ich bin sicher, dass Sie diesen Ansatz schon oft gesehen haben, zum Beispiel die deklarative Deklaration von Modellfeldern in Django:
class Book(models.Model): title = models.Charfield(max_length=250)
Im obigen Beispiel ist title
der Name des Python-Bezeichners. Er wird auch verwendet, um die Spalte in der book
zu benennen, obwohl wir dies nirgendwo explizit angegeben haben. Ja, solche „Magie“ kann mit Hilfe der Metaprogrammierung realisiert werden. Implementieren wir beispielsweise ein System zum Übertragen von Anwendungsfehlern an das Front-End, sodass jede Nachricht einen lesbaren Code enthält, mit dem die Nachricht in eine andere Sprache übersetzt werden kann. Wir haben also ein Nachrichtenobjekt, das in json
konvertiert werden kann:
class Message: def __init__(self, text, code=None): self.text = text self.code = code def to_json(self): return json.dumps({'text': self.text, 'code': self.code})
Alle unsere Fehlermeldungen werden in einem separaten "Namespace" gespeichert:
class Messages: not_found = Message('Resource not found') bad_request = Message('Request body is invalid') ... >>> Messages.not_found.to_json() {"text": "Resource not found", "code": null}
Jetzt möchten wir, dass der code
nicht null
, sondern nicht not_found
wird. Dazu schreiben wir die folgende Metaklasse:
class MetaMessage(type): def __new__(mcs, name, bases, attrs): for attr, value in attrs.items():
Mal sehen, wie unsere Beiträge jetzt aussehen:
>>> Messages.not_found.to_json() {"text": "Resource not found", "code": "not_found"} >>> Messages.bad_request.to_json() {"text": "Request body is invalid", "code": "bad_request"}
Was du brauchst! Jetzt wissen Sie, was zu tun ist, damit Sie anhand des Datenformats leicht den Code finden können, der sie verarbeitet.
Ein weiterer häufiger Fall ist das Zwischenspeichern statischer Daten in der Phase der Klassenerstellung, um keine Zeit mit der Berechnung zu verschwenden, während die Anwendung ausgeführt wird. Darüber hinaus können einige Daten aktualisiert werden, wenn neue Instanzen von Klassen erstellt werden, z. B. ein Zähler für die Anzahl der erstellten Objekte.
Wie kann das genutzt werden? Angenommen, Sie entwickeln ein Framework zum Erstellen von Berichten und Tabellen und haben ein solches Objekt:
class Row(metaclass=MetaRow): name: str age: int ... def __init__(self, **kwargs): self.counter = None for attr, value in kwargs.items(): setattr(self, attr, value) def __str__(self): out = [self.counter]
Wir möchten den Zähler beim Erstellen einer neuen Zeile speichern und erhöhen und den Header der resultierenden Tabelle im Voraus generieren. Metaklasse zur Rettung!
class MetaRow(type):
Zwei Dinge müssen hier geklärt werden:
- Die
Row
Klasse hat keine Klassenattribute mit den Namen name
und age
Dies sind attrs
, daher befinden sie sich nicht in den attrs
Wörterbuchschlüsseln. Um eine Liste der Felder zu erhalten, verwenden wir das __annotations__
. - Die Operation
cls.row_count += 1
sollte Sie irreführen: wie? Schließlich ist cls
eine Row
Klasse und hat nicht das Attribut row_count
. Alles ist wahr, aber wie ich oben erklärt habe - wenn die erstellte Klasse kein Attribut oder keine Methode hat, die sie aufrufen möchten, geht der Interpreter weiter entlang der Kette von Basisklassen - wenn keine darin enthalten sind, wird eine Suche in der Metaklasse durchgeführt. In solchen Fällen ist es besser, einen anderen Datensatz zu verwenden, um niemanden zu verwirren: MetaRow.row_count += 1
.
Sehen Sie, wie elegant Sie jetzt die gesamte Tabelle anzeigen können:
rows = [ Row(name='Valentin', age=25), Row(name='Sergey', age=33), Row(name='Gosha'), ] print(' | '.join(Row.__header__)) for row in rows: print(row)
№ | age | name 1 | 25 | Valentin 2 | 33 | Sergey 3 | N/A | Gosha
Das Anzeigen und Arbeiten mit einer Tabelle kann übrigens in einer separaten Sheet
Klasse zusammengefasst werden.
Fortsetzung folgt...
Im nächsten Teil dieses Artikels werde ich beschreiben, wie Sie Metaklassen zum Debuggen Ihres Anwendungscodes verwenden, wie Sie die Erstellung einer Metaklasse parametrisieren und grundlegende Beispiele für die Verwendung der __prepare__
-Methode zeigen. Bleib dran!
Ausführlicher über Metaklassen und Deskriptoren in Python werde ich im Rahmen von Advanced Python berichten .