Python v3.x: como aumentar a velocidade do decorador sem registro e sms

No começo era este artigo . Então um comentário apareceu nela . Como resultado, mergulhei na leitura do material, mergulhei no debag e pude otimizar o código da primeira parte desta história. Proponho caminhar comigo pelos pontos principais.

Primeiro, quero agradecer a Mogost . Graças ao seu comentário, redefini a abordagem do Python. Eu já tinha ouvido falar que havia muitos caras antieconômicos entre os pitonistas (quando se lida com memória), e agora aconteceu que, de alguma forma, eu me juntei a essa parte invisivelmente.

Então, vamos começar. Vamos especular sobre quais foram os gargalos em geral.

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


e este não é o limite. Portanto, removi parte do ifs, transferi algo para __init__, ou seja, para onde será chamado uma vez. Especificamente, a verificação de propriedade no código deve ser chamada uma vez, porque o decorador é aplicado ao método e atribuído a ele. E a propriedade da classe, respectivamente, permanecerá inalterada. Portanto, não há necessidade de verificar a propriedade constantemente.

Um ponto separado é se estiver dentro. O criador de perfil mostrou que cada um deles tem uma chamada separada, então decidi combinar todos os manipuladores em um ditado. Isso permitiu evitar ifs em geral, em vez de usar simplesmente:
 self.handlers.get(e.__class__, Exception)(e) 


assim, em self.handlers, temos um dict, que como padrão contém uma função que gera as outras exceções.

Obviamente, o invólucro merece atenção especial. Essa é a mesma função que é chamada toda vez que o decorador é chamado. I.e. aqui é melhor evitar verificações desnecessárias e todo tipo de carga ao máximo, se possível, colocando-as em __init__ ou em __call__. Aqui está o que o wrapper era antes:
 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) 


o número de verificações passa pelo telhado. Tudo isso será chamado em todas as chamadas para o decorador. Portanto, o wrapper ficou assim:
  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 


Lembre-se, __call__ será chamado uma vez. Dentro de __call__, dependendo do grau de assincronia da função, retornamos a própria função ou corotina. E também quero observar que a função asyncio.iscoroutine faz uma chamada adicional, então mudei para inspecionar. Na verdade, bancos (cProfile) para assíncio e inspecionam:

  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} 


Código completo:
 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) 


E provavelmente o exemplo ficaria incompleto sem tempo. Portanto, usando o exemplo do comentário acima:
 class MathWithTry(object): def divide(self, a, b): try: return a // b except ZeroDivisionError: return '   ,   ' 


e um exemplo do texto do artigo anterior ( ATENÇÃO! passamos e para o exemplo do texto no lambda. Esse não foi o caso no artigo anterior e foi adicionado apenas nas inovações):
 class Math(object): @property def exception_handlers(self): return { ZeroDivisionError: lambda e: '   ,   ' } @ProcessException(exception_handlers) def divide(self, a, b): return a // b 


Aqui estão os resultados para você:
 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 


Concluindo, quero dizer que a otimização, na minha opinião, é um processo bastante complicado, e aqui é importante não se deixar levar e não otimizar algo em detrimento da legibilidade. Caso contrário, debitar em otimizado será extremamente difícil.

Obrigado por seus comentários. Estou ansioso para comentários sobre este artigo também :)

PS, graças aos comentários dos usuários do Habr, foi possível acelerar ainda mais, foi o que aconteceu:
 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 


Acelerado em 0,03 em média. Obrigado a Kostiantyn e Yngvie .

PS Atualizado! Otimizei ainda mais o código com base nos comentários de onegreyonewhite e resetme . Substituiu self.func por apenas func e transformou self.handlers em uma variável. A execução foi acelerada ainda mais, especialmente se houver mais repetições por zero. Cito timeit:
 timeit.timeit('t.divide_with_decorator(1, 0)', number=1000000, setup='from __main__ import t') 1.1116105649998644 


Antes dessa otimização, a execução com o mesmo valor numérico levava 1,24 em média.

PS: Otimizei ainda mais introduzindo a função raise_exception de __init__ no @staticmethod e estou acessando-a através de uma variável para remover o acesso através de um ponto. Na verdade, o tempo médio de execução tornou-se:
 timeit.timeit('t.divide_with_decorator(1, 0)', number=1000000, setup='from __main__ import t') 1.0691639049982768 


isto é para o método E as funções são chamadas ainda mais rapidamente (em média):
 timeit.timeit('div(1, 0)', number=1000000, setup='from __main__ import div') 1.0463485610016505 

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


All Articles