Metaprogrammierung in Python meistern: Kontrolliere alles, was du willst
Grace Collins
Solutions Engineer · Leapcell

Erforschung der Metaprogrammierung in Python
Viele Leute sind mit dem Konzept der "Metaprogrammierung" nicht vertraut, und es gibt keine sehr präzise Definition dafür. Dieser Artikel konzentriert sich auf die Metaprogrammierung innerhalb von Python. In Wirklichkeit entspricht das, was hier diskutiert wird, jedoch möglicherweise nicht vollständig der strengen Definition von "Metaprogrammierung". Ich konnte nur keinen passenderen Begriff finden, um das Thema dieses Artikels darzustellen, daher habe ich mir diesen ausgeborgt.
Der Untertitel lautet "Kontrolliere alles, was du kontrollieren willst". Im Wesentlichen konzentriert sich dieser Artikel auf eines: die von Python bereitgestellten Funktionen zu nutzen, um den Code so elegant und prägnant wie möglich zu gestalten. Konkret modifizieren wir durch Programmiertechniken die Eigenschaften einer Abstraktion auf einer höheren Abstraktionsebene.
In erster Linie ist es ein bekanntes Klischee, dass in Python alles ein Objekt ist. Darüber hinaus bietet Python zahlreiche "Metaprogrammierungs" -Mechanismen, wie z. B. spezielle Methoden und Metaklassen. Operationen wie das dynamische Hinzufügen von Attributen und Methoden zu einem Objekt werden in Python überhaupt nicht als "Metaprogrammierung" betrachtet. In einigen statischen Sprachen erfordert dies jedoch bestimmte Fähigkeiten. Lassen Sie uns einige Aspekte besprechen, die Python -Programmierer leicht verwirren können.
Beginnen wir damit, Objekte in verschiedene Ebenen einzuteilen. Im Allgemeinen wissen wir, dass ein Objekt seinen Typ hat, und Python hat Typen seit langem als Objekte implementiert. Somit haben wir Instanzobjekte und Klassenobjekte, die zwei Ebenen sind. Leser mit einem grundlegenden Verständnis werden sich der Existenz von Metaklassen bewusst sein. Kurz gesagt, eine Metaklasse ist die "Klasse" einer "Klasse", was bedeutet, dass sie sich auf einer höheren Ebene als eine Klasse befindet. Dies fügt eine weitere Ebene hinzu. Gibt es noch mehr?
ImportTime vs RunTime
Wenn wir es aus einer anderen Perspektive betrachten und nicht die gleichen Kriterien wie die vorherigen drei Ebenen anwenden müssen, können wir zwischen zwei Konzepten unterscheiden: ImportTime und RunTime. Die Grenzen zwischen ihnen sind nicht eindeutig. Wie die Namen schon sagen, beziehen sie sich auf zwei Momente: den Zeitpunkt des Imports und den Zeitpunkt der Ausführung.
Was passiert, wenn ein Modul importiert wird? Anweisungen im globalen Bereich (nicht - definitorische Anweisungen) werden ausgeführt. Was ist mit Funktionsdefinitionen? Ein Funktionsobjekt wird erstellt, aber der Code darin wird nicht ausgeführt. Für Klassendefinitionen wird ein Klassenobjekt erstellt, der Code im Klassendefinitionsbereich wird ausgeführt und der Code in den Klassenmethoden wird natürlich nicht ausgeführt.
Was ist mit der Ausführung? Der Code in Funktionen und Methoden wird ausgeführt. Natürlich müssen Sie sie zuerst aufrufen.
Metaklassen
Wir können also sagen, dass Metaklassen und Klassen zu ImportTime gehören. Nachdem ein Modul importiert wurde, werden sie erstellt. Instanzobjekte gehören zu RunTime. Das einfache Importieren eines Moduls erstellt keine Instanzobjekte. Wir können jedoch nicht zu dogmatisch sein, denn wenn Sie eine Klasse innerhalb des Modulbereichs instanziieren, werden auch Instanzobjekte erstellt. Es ist nur so, dass wir die Instanziierung normalerweise innerhalb von Funktionen schreiben, daher diese Klassifizierung.
Was sollten Sie tun, wenn Sie die Eigenschaften der erstellten Instanzobjekte steuern möchten? Es ist ganz einfach. Überschreiben Sie die Methode __init__
in der Klassendefinition. Was aber, wenn wir einige Eigenschaften der Klasse steuern möchten? Gibt es einen solchen Bedarf? Auf jeden Fall!
Bezüglich des klassischen Singleton -Musters weiß jeder, dass es mehrere Möglichkeiten gibt, es zu implementieren. Die Anforderung ist, dass eine Klasse nur eine Instanz haben kann.
Die einfachste Implementierung ist wie folgt:
class _Spam: def __init__(self): print("Spam!!!") _spam_singleton = None def Spam(): global _spam_singleton if _spam_singleton is not None: return _spam_singleton else: _spam_singleton = _Spam() return _spam_singleton
Dieses fabrikartige Muster ist nicht sehr elegant. Lassen Sie uns die Anforderung noch einmal überprüfen. Wir möchten, dass eine Klasse nur eine Instanz hat. Die Methoden, die wir in einer Klasse definieren, sind die Verhaltensweisen von Instanzobjekten. Wenn wir also das Verhalten einer Klasse ändern möchten, benötigen wir etwas auf einer höheren Ebene. Hier kommen Metaklassen ins Spiel. Wie bereits erwähnt, ist eine Metaklasse die Klasse einer Klasse. Das heißt, die Methode __init__
einer Metaklasse ist die Initialisierungsmethode einer Klasse. Wir wissen, dass es auch die Methode __call__
gibt, die es einer Instanz ermöglicht, wie eine Funktion aufgerufen zu werden. Dann ist diese Methode einer Metaklasse diejenige, die aufgerufen wird, wenn eine Klasse instanziiert wird.
Der Code kann wie folgt geschrieben werden:
class Singleton(type): def __init__(self, *args, **kwargs): self._instance = None super().__init__(*args, **kwargs) def __call__(self, *args, **kwargs): if self._instance is None: self._instance = super().__call__(*args, **kwargs) return self._instance else: return self._instance class Spam(metaclass = Singleton): def __init__(self): print("Spam!!!")
Es gibt zwei Hauptunterschiede im Vergleich zu einer allgemeinen Klassendefinition. Der eine ist, dass die Basisklasse von Singleton
type
ist, und der andere ist, dass es in der Definition von Spam
ein metaclass = Singleton
gibt. Was ist type
? Es ist eine Unterklasse von object
, und object
ist seine Instanz. Das heißt, type
ist die Klasse aller Klassen, die grundlegendste Metaklasse. Es legt einige Operationen fest, die alle Klassen benötigen, wenn sie erstellt werden. Unsere benutzerdefinierte Metaklasse muss also type
ableiten. Gleichzeitig ist type
auch ein Objekt, also ist es eine Unterklasse von object
. Es ist etwas schwer zu fassen, aber machen Sie sich nur eine allgemeine Vorstellung.
Dekorateure
Lassen Sie uns über Dekorateure sprechen. Die meisten Leute halten Dekorateure für eines der am schwierigsten zu verstehenden Konzepte in Python. Tatsächlich ist es nur syntaktischer Zucker. Sobald Sie verstanden haben, dass Funktionen auch Objekte sind, können Sie ganz einfach Ihre eigenen Dekorateure schreiben.
from functools import wraps def print_result(func): @wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) print(result) return result return wrapper @print_result def add(x, y): return x + y # Äquivalent zu: # add = print_result(add) add(1, 3)
Hier verwenden wir auch einen Dekorateur @wraps
, der verwendet wird, um sicherzustellen, dass die zurückgegebene innere Funktion wrapper
die gleiche Funktionssignatur wie die ursprüngliche Funktion hat. Grundsätzlich sollten wir es beim Schreiben von Dekorateuren hinzufügen.
Wie ich in die Kommentare geschrieben habe, entspricht die Form von @decorator
func = decorator(func)
. Das Verständnis dieses Punktes ermöglicht es uns, weitere Arten von Dekorateuren zu schreiben. Zum Beispiel Klassendekorateure und das Schreiben eines Dekorateurs als Klasse.
def attr_upper(cls): for attrname, value in cls.__dict__.items(): if isinstance(value, str): if not value.startswith('__'): setattr(cls, attrname, bytes.decode(str.encode(value).upper())) return cls @attr_upper class Person: sex ='man' print(Person.sex) # MAN
Beachten Sie die Unterschiede zwischen der Implementierung von gewöhnlichen Dekorateuren und Klassendekorateuren.
Datenabstraktion - Deskriptoren
Wenn wir möchten, dass einige Klassen bestimmte gemeinsame Merkmale aufweisen oder in der Lage sind, sie innerhalb der Klassendefinition zu steuern, können wir eine Metaklasse anpassen und sie zur Metaklasse dieser Klassen machen. Wenn wir möchten, dass einige Funktionen bestimmte gemeinsame Funktionen haben und Codeduplizierung vermeiden, können wir einen Dekorateur definieren. Was aber, wenn wir möchten, dass die Attribute von Instanzen bestimmte gemeinsame Merkmale aufweisen? Einige mögen sagen, dass wir property
verwenden können, und das können wir in der Tat. Diese Logik muss aber in jeder Klassendefinition geschrieben werden. Wenn wir möchten, dass einige Attribute der Instanzen dieser Klassen die gleichen Merkmale aufweisen, können wir eine Deskriptorklasse anpassen.
In Bezug auf Deskriptoren erklärt dieser Artikel https://docs.python.org/3/howto/descriptor.html dies sehr gut. Gleichzeitig wird auch erläutert, wie Deskriptoren hinter Funktionen versteckt sind, um die Vereinheitlichung und die Unterschiede zwischen Funktionen und Methoden zu erreichen. Hier sind einige Beispiele.
class TypedField: def __init__(self, _type): self._type = _type def __get__(self, instance, cls): if instance is None: return self else: return getattr(instance, self.name) def __set_name__(self, cls, name): self.name = name def __set__(self, instance, value): if not isinstance(value, self._type): raise TypeError('Expected' + str(self._type)) instance.__dict__[self.name] = value class Person: age = TypedField(int) name = TypedField(str) def __init__(self, age, name): self.age = age self.name = name jack = Person(15, 'Jack') jack.age = '15' # Wird einen Fehler auslösen
Es gibt hier mehrere Rollen. TypedField
ist eine Deskriptorklasse, und die Attribute von Person
sind Instanzen der Deskriptorklasse. Es scheint, dass der Deskriptor als Attribut von Person
existiert, das heißt, als ein Klassenattribut und nicht als ein Instanzattribut. Aber sobald eine Instanz von Person
auf ein Attribut mit dem gleichen Namen zugreift, wird der Deskriptor wirksam. Es ist zu beachten, dass es in Python 3.5 und früheren Versionen keine spezielle Methode __set_name__
gibt. Dies bedeutet, dass Sie, wenn Sie wissen möchten, welchen Namen der Deskriptor in der Klassendefinition erhält, ihn explizit an den Deskriptor übergeben müssen, wenn Sie ihn instanziieren, das heißt, Sie benötigen einen weiteren Parameter. In Python 3.6 wird dieses Problem jedoch gelöst. Sie müssen nur die Methode __set_name__
in der Deskriptorklassendefinition überschreiben. Beachten Sie auch das Schreiben von __get__
. Grundsätzlich ist die Beurteilung von instance
notwendig, da sonst ein Fehler ausgelöst wird. Der Grund ist nicht schwer zu verstehen, daher werde ich nicht näher darauf eingehen.
Steuern der Erstellung von Unterklassen - Eine Alternative zu Metaklassen
In Python 3.6 können wir die Erstellung von Unterklassen anpassen, indem wir die spezielle Methode __init_subclass__
implementieren. Auf diese Weise können wir in einigen Fällen die etwas umständlichen Metaklassen vermeiden.
class PluginBase: subclasses = [] def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) cls.subclasses.append(cls) class Plugin1(PluginBase): pass class Plugin2(PluginBase): pass
Zusammenfassung
Metaprogrammierungstechniken wie Metaklassen sind für die meisten Menschen etwas obskur und schwer zu verstehen, und meistens müssen wir sie nicht verwenden. Die Implementierung der meisten Frameworks nutzt diese Techniken jedoch so, dass der von den Benutzern geschriebene Code prägnant und leicht verständlich ist. Wenn Sie ein tieferes Verständnis dieser Techniken erlangen möchten, können Sie sich auf einige Bücher wie Fluent Python und Python Cookbook beziehen (einige Inhalte dieses Artikels stammen aus diesen), oder einige Kapitel in der offiziellen Dokumentation lesen, wie z. B. den oben erwähnten Deskriptor How - To und den Abschnitt Datenmodell usw. Oder untersuchen Sie direkt den Python -Quellcode, einschließlich des in Python geschriebenen Quellcodes und des CPython -Quellcodes.
Denken Sie daran, diese Techniken erst zu verwenden, nachdem Sie sie vollständig verstanden haben, und versuchen Sie nicht, sie überall zu verwenden.
Leapcell: Die beste Serverlose Plattform für Webhosting
Abschließend möchte ich eine Plattform Leapcell empfehlen, die sich sehr gut für die Bereitstellung von Python -Diensten eignet.
1. Unterstützung mehrerer Sprachen
- Entwickeln Sie mit JavaScript, Python, Go oder Rust.
2. Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung - keine Anfragen, keine Gebühren.
3. Unschlagbare Kosteneffizienz
- Pay - as - you - go ohne Leerlaufgebühren.
- Beispiel: 25 $ unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
4. Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD -Pipelines und GitOps -Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
5. Mühelose Skalierbarkeit und hohe Leistung
- Auto -Skalierung zur einfachen Bewältigung hoher Parallelität.
- Kein Betriebsaufwand - konzentrieren Sie sich einfach auf das Erstellen.
Entdecken Sie mehr in der Dokumentation!
Leapcell Twitter: https://x.com/LeapcellHQ