Python v3.x: comment augmenter la vitesse du décorateur sans inscription et sms

Au début était cet article . Puis un commentaire lui est apparu. En conséquence, je me suis plongé dans la lecture du matériel, j'ai creusé dans debag et j'ai pu optimiser le code de la première partie de cette histoire. Je propose de marcher avec moi le long des points principaux.

Tout d'abord, je tiens à remercier Mogost . Grâce à son commentaire, j'ai redéfini l'approche de Python. J'avais déjà entendu dire qu'il y avait beaucoup de mecs non rentables parmi les pythonistes (lorsqu'ils traitaient avec la mémoire), et maintenant il s'est avéré que j'avais en quelque sorte invisible rejoint cette fête.

Commençons donc. Imaginons quels étaient les goulots d'étranglement en général.

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


et ce n'est pas la limite. Par conséquent, j'ai supprimé une partie des ifs, transféré quelque chose à __init__, c'est-à-dire à l'endroit où il sera appelé une fois. Plus précisément, la vérification de la propriété dans le code doit être appelée une fois, car le décorateur est appliqué à la méthode et lui est affecté. Et la propriété de la classe, respectivement, restera inchangée. Par conséquent, il n'est pas nécessaire de vérifier constamment la propriété.

Un point distinct est si. Le profileur a montré que chacun d'eux avait un appel distinct, j'ai donc décidé de combiner tous les gestionnaires en un seul dict. Cela a permis d'éviter les ifs en général, au lieu d'utiliser simplement:
 self.handlers.get(e.__class__, Exception)(e) 


ainsi, dans self.handlers, nous avons un dict qui, par défaut, contient une fonction qui déclenche les autres exceptions.

Bien sûr, le wrapper mérite une attention particulière. C'est la même fonction qui est appelée à chaque fois que le décorateur est appelé. C'est-à-dire ici, il vaut mieux éviter les vérifications inutiles et toutes sortes de charges au maximum, si possible, en les mettant dans __init__ ou dans __call__. Voici ce qu'était le wrapper avant:
 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) 


le nombre de contrôles passe par le toit. Tout cela sera appelé à chaque appel du décorateur. Par conséquent, le wrapper est devenu comme ceci:
  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 


rappelez-vous, __call__ sera appelé une fois. À l'intérieur de __call__, selon le degré d'asynchronie de la fonction, nous renvoyons la fonction elle-même ou coroutine. Et je tiens également à noter que asyncio.iscoroutinefunction effectue un appel supplémentaire, alors je suis passé à inspect.iscoroutinefunction. En fait, des bancs (cProfile) pour asyncio et inspectent:

  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} 


Code complet:
 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) 


Et probablement l'exemple serait incomplet sans timeit. Par conséquent, en utilisant l'exemple du commentaire ci-dessus:
 class MathWithTry(object): def divide(self, a, b): try: return a // b except ZeroDivisionError: return '   ,   ' 


et un exemple du texte de l' article précédent ( ATTENTION! nous passons e à l'exemple du texte de la lambda. Ce n'était pas le cas dans l'article précédent et n'a été ajouté que dans les nouveautés):
 class Math(object): @property def exception_handlers(self): return { ZeroDivisionError: lambda e: '   ,   ' } @ProcessException(exception_handlers) def divide(self, a, b): return a // b 


voici les résultats pour vous:
 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 


En conclusion, je tiens à dire que l'optimisation, à mon avis, est un processus assez compliqué, et ici il est important de ne pas se laisser emporter et de ne pas optimiser quelque chose au détriment de la lisibilité. Sinon, le débit sur optimisé sera extrêmement difficile.

Merci pour vos commentaires. J'attends aussi avec impatience les commentaires sur cet article :)

PS grâce aux commentaires des utilisateurs du Habr il a été possible d'accélérer encore plus, c'est ce qui s'est passé:
 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 


Accéléré de 0,03 en moyenne. Merci à Kostiantyn et Yngvie .

PS mis à jour! J'ai encore optimisé le code sur la base des commentaires des commentaires de onegreyonewhite et resetme . Remplacé self.func par juste func et transformé self.handlers en variable. L'exécution a été encore accélérée, particulièrement visible s'il y a plus de répétitions par zéro. Je cite timeit:
 timeit.timeit('t.divide_with_decorator(1, 0)', number=1000000, setup='from __main__ import t') 1.1116105649998644 


Avant cette optimisation, l'exécution avec la même valeur numérique prenait 1,24 en moyenne.

PS J'ai encore optimisé en introduisant la fonction raise_exception de __init__ dans @staticmethod et j'y accède via une variable pour supprimer l'accès via un point. En fait, le temps d'exécution moyen est devenu:
 timeit.timeit('t.divide_with_decorator(1, 0)', number=1000000, setup='from __main__ import t') 1.0691639049982768 


c'est pour la méthode. Et les fonctions sont appelées encore plus rapidement (en moyenne):
 timeit.timeit('div(1, 0)', number=1000000, setup='from __main__ import div') 1.0463485610016505 

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


All Articles