Python: Metaprogrammierung in der Produktion. Teil zwei

Wir sprechen weiterhin über Metaprogrammierung in Python. Bei korrekter Verwendung können Sie komplexe Entwurfsmuster schnell und elegant implementieren. Im letzten Teil dieses Artikels haben wir gezeigt, wie Metaklassen verwendet werden können, um die Attribute von Instanzen und Klassen zu ändern.



Nun wollen wir sehen, wie Sie Methodenaufrufe ändern können. Weitere Informationen zu Metaprogrammierungsoptionen finden Sie im Advanced Python- Kurs.


Debuggen und Verfolgen von Anrufen


Wie Sie bereits verstanden haben, kann mit einer Metaklasse jede Klasse bis zur Unkenntlichkeit transformiert werden. Ersetzen Sie beispielsweise alle Klassenmethoden durch andere oder wenden Sie auf jede Methode einen beliebigen Dekorator an. Mit dieser Idee können Sie die Anwendungsleistung debuggen.


Die folgende Metaklasse misst die Ausführungszeit jeder Methode in der Klasse und ihren Instanzen sowie die Erstellungszeit der Instanz selbst:


from contextlib import contextmanager import logging import time import wrapt @contextmanager def timing_context(operation_name): """       """ start_time = time.time() try: yield finally: logging.info('Operation "%s" completed in %0.2f seconds', operation_name, time.time() - start_time) @wrapt.decorator def timing(func, instance, args, kwargs): """       .     https://wrapt.readthedocs.io/en/latest/         """ with timing_context(func.__name__): return func(*args, **kwargs) class DebugMeta(type): def __new__(mcs, name, bases, attrs): for attr, method in attrs.items(): if not attr.startswith('_'): #     attrs[attr] = timing(method) return super().__new__(mcs, name, bases, attrs) def __call__(cls, *args, **kwargs): with timing_context(f'{cls.__name__} instance creation'): #      return super().__call__(*args, **kwargs) 

Schauen wir uns das Debuggen in Aktion an:


 class User(metaclass=DebugMeta): def __init__(self, name): self.name = name time.sleep(.7) def login(self): time.sleep(1) def logout(self): time.sleep(2) @classmethod def create(cls): time.sleep(.5) user = User('Michael') user.login() user.logout() user.create() #   INFO:__main__:Operation "User instance creation" completed in 0.70 seconds INFO:__main__:Operation "login" completed in 1.00 seconds INFO:__main__:Operation "logout" completed in 2.00 seconds INFO:__main__:Operation "create" completed in 0.50 seconds 

Versuchen Sie, DebugMeta und die Signatur der Methoden und deren Stack-Trace zu protokollieren.


Das Einzelgängermuster und das Erbverbot


Kommen wir nun zu exotischen Fällen der Verwendung von Metaklassen in Python-Projekten.


Sicherlich verwenden viele von Ihnen das übliche Python-Modul, um ein Singleton- Entwurfsmuster (auch bekannt als Singleton) zu implementieren, da es viel bequemer und schneller ist als das Schreiben der entsprechenden Metaklasse. Lassen Sie uns jedoch eine seiner Implementierungen aus Gründen des akademischen Interesses schreiben:


 class Singleton(type): instance = None def __call__(cls, *args, **kwargs): if cls.instance is None: cls.instance = super().__call__(*args, **kwargs) return cls.instance class User(metaclass=Singleton): def __init__(self, name): self.name = name def __repr__(self): return f'<User: {self.name}>' u1 = User('Pavel') #         u2 = User('Stepan') >>> id(u1) == id(u2) True >>> u2 <User: Pavel> >>> User.instance <User: Pavel> #   , ? >>> u1.instance.instance.instance.instance <User: Pavel> 

Diese Implementierung hat eine interessante Nuance: Da der Klassenkonstruktor nicht zum zweiten Mal aufgerufen wird, können Sie einen Fehler machen und den erforderlichen Parameter dort nicht übergeben. Zur Laufzeit geschieht nichts, wenn die Instanz bereits erstellt wurde. Zum Beispiel:


 >>> User('Roman') <User: Roman> >>> User('Alexey', 'Petrovich', 66) #     ! <User: Roman> #     User       #    TypeError! 

Schauen wir uns nun eine noch exotischere Option an: ein Verbot der Vererbung von einer bestimmten Klasse.


 class FinalMeta(type): def __new__(mcs, name, bases, attrs): for cls in bases: if isinstance(cls, FinalMeta): raise TypeError(f"Can't inherit {name} class from final {cls.__name__}") return super().__new__(mcs, name, bases, attrs) class A(metaclass=FinalMeta): """   !""" pass class B(A): pass # TypeError: Can't inherit B class from final A #    ! 

Metaklassenparametrierung


In den vorherigen Beispielen haben wir Metaklassen verwendet, um die Erstellung von Klassen anzupassen. Sie können jedoch noch weiter gehen und mit der Parametrisierung des Verhaltens von Metaklassen beginnen.


Beispielsweise können Sie beim Deklarieren einer Klasse eine Funktion an den Metaklassenparameter übergeben und abhängig von bestimmten Bedingungen verschiedene Instanzen von Metaklassen zurückgeben, z. B.:


 def get_meta(name, bases, attrs): if SOME_SETTING: return MetaClass1(name, bases, attrs) else: return MetaClass2(name, bases, attrs) class A(metaclass=get_meta): pass 

Ein interessanteres Beispiel ist jedoch die Verwendung von extra_kwargs Parametern beim Deklarieren von Klassen. Angenommen, Sie möchten die Metaklasse verwenden, um das Verhalten bestimmter Methoden in einer Klasse zu ändern, und jede Klasse hat möglicherweise unterschiedliche Namen für diese Methoden. Was tun? Und hier ist was


 #   `DebugMeta`     class DebugMetaParametrized(type): def __new__(mcs, name, bases, attrs, **extra_kwargs): debug_methods = extra_kwargs.get('debug_methods', ()) for attr, value in attrs.items(): #      ,   #    `debug_methods`: if attr in debug_methods: attrs[attr] = timing(value) return super().__new__(mcs, name, bases, attrs) class User(metaclass=DebugMetaParametrized, debug_methods=('login', 'create')): ... user = User('Oleg') user.login() #  "logout"   . user.logout() user.create() 

Meiner Meinung nach ist es sehr elegant geworden! Sie können sich viele Muster für die Verwendung dieser Parametrisierung einfallen lassen, aber denken Sie an die Hauptregel - alles ist in Maßen gut.


__prepare__ Methodenbeispiele


Abschließend werde ich über die mögliche Verwendung der __prepare__ Methode __prepare__ . Wie oben erwähnt, sollte diese Methode ein Wörterbuchobjekt zurückgeben, das der Interpreter zum Zeitpunkt des Parsens des Klassenkörpers ausfüllt. Wenn __prepare__ beispielsweise das Objekt d = dict() zurückgibt, dann beim Lesen der folgenden Klasse:


 class A: x = 12 y = 'abc' z = {1: 2} 

Der Interpreter führt die folgenden Operationen aus:


 d['x'] = 12 d['y'] = 'abc' d['z'] = {1: 2} 

Es gibt mehrere Verwendungsmöglichkeiten für diese Funktion. Sie sind alle von unterschiedlichem Nutzen, also:


  1. Wenn wir in Versionen von Python = <3.5 die Reihenfolge der Deklaration von Methoden in einer Klasse __prepare__ , können wir collections.OrderedDict __prepare__ von der __prepare__ -Methode. In älteren Versionen __prepare__ integrierte Wörterbücher bereits die Reihenfolge des Hinzufügens von Schlüsseln bei, sodass OrderedDict nicht mehr benötigt wird.
  2. Das enum Standardbibliotheksmodul verwendet ein benutzerdefiniertes diktartiges Objekt, um zu bestimmen, wann ein Klassenattribut bei der Deklaration dupliziert wird. Den Code finden Sie hier .
  3. Überhaupt kein produktionsbereiter Code, aber ein sehr gutes Beispiel ist die Unterstützung des parametrischen Polymorphismus .

Betrachten Sie beispielsweise die folgende Klasse mit drei Implementierungen einer einzelnen polymorphen Methode:


 class Terminator: def terminate(self, x: int): print(f'Terminating INTEGER {x}') def terminate(self, x: str): print(f'Terminating STRING {x}') def terminate(self, x: dict): print(f'Terminating DICTIONARY {x}') t1000 = Terminator() t1000.terminate(10) t1000.terminate('Hello, world!') t1000.terminate({'hello': 'world'}) #  Terminating DICTIONARY 10 Terminating DICTIONARY Hello, world! Terminating DICTIONARY {'hello': 'world'} 

Offensichtlich überschreibt die zuletzt deklarierte terminate Methode die Implementierungen der ersten beiden, und wir müssen die Methode abhängig von der Art des übergebenen Arguments auswählen. Um dies zu erreichen, programmieren wir einige zusätzliche Wrapper-Objekte:


 class PolyDict(dict): """ ,               PolyMethod. """ def __setitem__(self, key: str, func): if not key.startswith('_'): if key not in self: super().__setitem__(key, PolyMethod()) self[key].add_implementation(func) return None return super().__setitem__(key, func) class PolyMethod: """    ,            .       ,       : instance method, staticmethod, classmethod. """ def __init__(self): self.implementations = {} self.instance = None self.cls = None def __get__(self, instance, cls): self.instance = instance self.cls = cls return self def _get_callable_func(self, impl): # ""  classmethod/staticmethod return getattr(impl, '__func__', impl) def __call__(self, arg): impl = self.implementations[type(arg)] callable_func = self._get_callable_func(impl) if isinstance(impl, staticmethod): return callable_func(arg) elif self.cls and isinstance(impl, classmethod): return callable_func(self.cls, arg) else: return callable_func(self.instance, arg) def add_implementation(self, func): callable_func = self._get_callable_func(func) #   ,     1  arg_name, arg_type = list(callable_func.__annotations__.items())[0] self.implementations[arg_type] = func 

Das Interessanteste im obigen Code ist das PolyMethod Objekt, in dem je nach Art des an diese Methode übergebenen Arguments eine Registrierung mit Implementierungen derselben Methode PolyMethod wird. Wir werden das PolyDict Objekt von der __prepare__ Methode zurückgeben und dabei verschiedene Implementierungen der Methoden mit dem gleichen Namen terminate speichern. Ein wichtiger Punkt: Beim Lesen des Klassenkörpers und beim Erstellen des attrs Objekts platziert der Interpreter die sogenannten unbound Funktionen dort. Diese Funktionen wissen noch nicht, auf welche Klasse oder Instanz sie aufgerufen werden. Wir mussten ein Deskriptorprotokoll implementieren, um den Kontext während des Funktionsaufrufs zu definieren und entweder self oder cls als ersten Parameter zu übergeben oder nichts zu übergeben, wenn staticmethod aufgerufen wird.


Als Ergebnis werden wir die folgende Magie sehen:


 class PolyMeta(type): @classmethod def __prepare__(mcs, name, bases): return PolyDict() class Terminator(metaclass=PolyMeta): ... t1000 = Terminator() t1000.terminate(10) t1000.terminate('Hello, world!') t1000.terminate({'hello': 'world'}) #  Terminating INTEGER 10 Terminating STRING Hello, world! Terminating DICTIONARY {'hello': 'world'} >>> t1000.terminate <__main__.PolyMethod object at 0xdeadcafe> 

Wenn Sie andere interessante Anwendungen der __prepare__ -Methode kennen, schreiben Sie bitte in die Kommentare.


Fazit


Metaprogrammierung ist eines von vielen Themen, über die ich in Advanced Python gesprochen habe . Im Rahmen des Kurses werde ich Ihnen auch erklären, wie Sie die Prinzipien von SOLID und GRASP bei der Entwicklung großer Python-Projekte effektiv einsetzen, die Anwendungsarchitektur entwerfen und leistungsstarken und qualitativ hochwertigen Code schreiben können. Ich werde mich freuen, Sie in den Mauern des Binary District zu sehen!

Source: https://habr.com/ru/post/de422415/


All Articles