Python v3.x: So erhöhen Sie die Geschwindigkeit von Dekorateuren ohne Registrierung und SMS

Am Anfang war dieser Artikel . Dann erschien ein Kommentar zu ihr . Infolgedessen habe ich mich mit dem Lesen des Materials befasst, mich mit Debag befasst und konnte den Code aus dem ersten Teil dieser Geschichte optimieren. Ich schlage vor, mit mir die Hauptpunkte entlang zu gehen.

Zunächst möchte ich Mogost danken. Dank seines Kommentars habe ich die Herangehensweise an Python neu definiert. Ich hatte zuvor gehört, dass es unter den Pythonisten viele unwirtschaftliche Typen gab (wenn es um das Gedächtnis ging), und jetzt stellte sich heraus, dass ich irgendwie unsichtbar dieser Partei beigetreten war.

Also fangen wir an. Spekulieren wir über die Engpässe im Allgemeinen.

Anhaltend wenn:
if isinstance(self.custom_handlers, property): if self.custom_handlers and e.__class__ in self.custom_handlers: if e.__class__ not in self.exclude: 


und das ist nicht die Grenze. Daher habe ich einen Teil des Wenns entfernt und etwas an __init__ übertragen, d. H. dorthin, wo es einmal aufgerufen wird. Insbesondere sollte die Überprüfung auf Eigenschaften im Code einmal aufgerufen werden, da Der Dekorateur wird auf die Methode angewendet und ihr zugewiesen. Das Eigentum der Klasse bleibt unverändert. Daher ist es nicht erforderlich, die Immobilie ständig zu überprüfen.

Ein separater Punkt ist wenn in. Der Profiler hat gezeigt, dass jeder dieser Eingaben einen eigenen Aufruf hat, daher habe ich beschlossen, alle Handler in einem Diktat zu kombinieren. Dies ermöglichte es, Wenns im Allgemeinen zu vermeiden, anstatt einfach zu verwenden:
 self.handlers.get(e.__class__, Exception)(e) 


In self.handlers haben wir also ein Diktat, das standardmäßig eine Funktion enthält, die die anderen Ausnahmen auslöst.

Natürlich verdient die Verpackung besondere Aufmerksamkeit. Dies ist dieselbe Funktion, die jedes Mal aufgerufen wird, wenn der Dekorateur aufgerufen wird. Das heißt Hier ist es besser, unnötige Überprüfungen und alle Arten von Lasten so weit wie möglich zu vermeiden, indem Sie sie in __init__ oder in __call__ ausgeben. Hier ist, was Wrapper vorher war:
 def wrapper(self, *args, **kwargs): if self.custom_handlers: if isinstance(self.custom_handlers, property): self.custom_handlers = self.custom_handlers.__get__(self, self.__class__) if asyncio.iscoroutinefunction(self.func): return self._coroutine_exception_handler(*args, **kwargs) else: return self._sync_exception_handler(*args, **kwargs) 


Die Anzahl der Schecks geht durch das Dach. Dies alles wird bei jedem Anruf beim Dekorateur aufgerufen. Daher wurde Wrapper so:
  def __call__(self, func): self.func = func if iscoroutinefunction(self.func): def wrapper(*args, **kwargs): return self._coroutine_exception_handler(*args, **kwargs) else: def wrapper(*args, **kwargs): return self._sync_exception_handler(*args, **kwargs) return wrapper 


Denken Sie daran, __call__ wird einmal aufgerufen. Innerhalb von __call__ geben wir abhängig vom Grad der Asynchronität der Funktion die Funktion selbst oder die Coroutine zurück. Außerdem möchte ich darauf hinweisen, dass die Funktion asyncio.iscoroutine einen zusätzlichen Aufruf ausführt, sodass ich zu inspect.iscoroutinefunction gewechselt bin. Eigentlich Bänke (cProfile) für Asyncio und inspizieren:

  ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 0.000 0.000 <string>:1(<module>) 1 0.000 0.000 0.000 0.000 coroutines.py:160(iscoroutinefunction) 1 0.000 0.000 0.000 0.000 inspect.py:158(isfunction) 1 0.000 0.000 0.000 0.000 inspect.py:179(iscoroutinefunction) 1 0.000 0.000 0.000 0.000 {built-in method builtins.exec} 1 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 


  ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 0.000 0.000 <string>:1(<module>) 1 0.000 0.000 0.000 0.000 inspect.py:158(isfunction) 1 0.000 0.000 0.000 0.000 inspect.py:179(iscoroutinefunction) 1 0.000 0.000 0.000 0.000 {built-in method builtins.exec} 1 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} 


Vollständiger Code:
 from inspect import iscoroutinefunction from asyncio import QueueEmpty, QueueFull from concurrent.futures import TimeoutError class ProcessException(object): __slots__ = ('func', 'handlers') def __init__(self, custom_handlers=None): self.func = None if isinstance(custom_handlers, property): custom_handlers = custom_handlers.__get__(self, self.__class__) def raise_exception(e: Exception): raise e exclude = { QueueEmpty: lambda e: None, QueueFull: lambda e: None, TimeoutError: lambda e: None } self.handlers = { **exclude, **(custom_handlers or {}), Exception: raise_exception } def __call__(self, func): self.func = func if iscoroutinefunction(self.func): def wrapper(*args, **kwargs): return self._coroutine_exception_handler(*args, **kwargs) else: def wrapper(*args, **kwargs): return self._sync_exception_handler(*args, **kwargs) return wrapper async def _coroutine_exception_handler(self, *args, **kwargs): try: return await self.func(*args, **kwargs) except Exception as e: return self.handlers.get(e.__class__, Exception)(e) def _sync_exception_handler(self, *args, **kwargs): try: return self.func(*args, **kwargs) except Exception as e: return self.handlers.get(e.__class__, Exception)(e) 


Und wahrscheinlich wäre das Beispiel ohne Zeit unvollständig. Verwenden Sie daher das Beispiel aus dem obigen Kommentar:
 class MathWithTry(object): def divide(self, a, b): try: return a // b except ZeroDivisionError: return '   ,   ' 


und ein Beispiel aus dem Text des vorherigen Artikels ( ACHTUNG! Wir übergeben e dem Beispiel aus dem Text im Lambda. Im vorherigen Artikel war dies nicht und wurde nur in den Neuerungen hinzugefügt):
 class Math(object): @property def exception_handlers(self): return { ZeroDivisionError: lambda e: '   ,   ' } @ProcessException(exception_handlers) def divide(self, a, b): return a // b 


Hier sind die Ergebnisse für Sie:
 timeit.timeit('math_with_try.divide(1, 0)', number=100000, setup='from __main__ import math_with_try') 0.05079065300014918 timeit.timeit('math_with_decorator.divide(1, 0)', number=100000, setup='from __main__ import math_with_decorator') 0.16211646200099494 


Abschließend möchte ich sagen, dass die Optimierung meiner Meinung nach ein ziemlich komplizierter Prozess ist, und hier ist es wichtig, sich nicht mitreißen zu lassen und etwas nicht zu Lasten der Lesbarkeit zu optimieren. Andernfalls ist die Abbuchung auf optimiert äußerst schwierig.

Vielen Dank für Ihre Kommentare. Ich freue mich auch auf Kommentare zu diesem Artikel :)

PS Dank der Kommentare der Nutzer des Habr konnte noch mehr beschleunigt werden, genau das ist passiert:
 from inspect import iscoroutinefunction from asyncio import QueueEmpty, QueueFull from concurrent.futures import TimeoutError class ProcessException(object): __slots__ = ('handlers',) def __init__(self, custom_handlers=None): if isinstance(custom_handlers, property): custom_handlers = custom_handlers.__get__(self, self.__class__) raise_exception = ProcessException.raise_exception exclude = { QueueEmpty: lambda e: None, QueueFull: lambda e: None, TimeoutError: lambda e: None } self.handlers = { **exclude, **(custom_handlers or {}), Exception: raise_exception } def __call__(self, func): handlers = self.handlers if iscoroutinefunction(func): async def wrapper(*args, **kwargs): try: return await func(*args, **kwargs) except Exception as e: return handlers.get(e.__class__, handlers[Exception])(e) else: def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: return handlers.get(e.__class__, handlers[Exception])(e) return wrapper @staticmethod def raise_exception(e: Exception): raise e 


 timeit.timeit('divide(1, 0)', number=100000, setup='from __main__ import divide') 0.13714907199755544 


Im Durchschnitt um 0,03 beschleunigt. Vielen Dank an Kostiantyn und Yngvie .

PS aktualisiert! Ich habe den Code basierend auf Kommentaren aus den Kommentaren von onegreyonewhite und resetme weiter optimiert. Ersetzte self.func durch nur func und machte self.handlers zu einer Variablen. Die Ausführung wurde weiter beschleunigt, was sich insbesondere bei mehr Wiederholungen pro Null bemerkbar macht. Ich zitiere timeit:
 timeit.timeit('t.divide_with_decorator(1, 0)', number=1000000, setup='from __main__ import t') 1.1116105649998644 


Vor dieser Optimierung dauerte die Ausführung mit demselben Zahlenwert durchschnittlich 1,24.

PS Ich habe noch mehr optimiert, indem ich die Funktion raise_exception von __init__ in @staticmethod eingeführt habe, und ich greife über eine Variable darauf zu, um den Zugriff über einen Punkt zu entfernen. Tatsächlich ist die durchschnittliche Ausführungszeit geworden:
 timeit.timeit('t.divide_with_decorator(1, 0)', number=1000000, setup='from __main__ import t') 1.0691639049982768 


Dies ist für die Methode. Und Funktionen werden (im Durchschnitt) noch schneller aufgerufen:
 timeit.timeit('div(1, 0)', number=1000000, setup='from __main__ import div') 1.0463485610016505 

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


All Articles