Python v3.x: cómo aumentar la velocidad del decorador sin registro y sms

Al principio fue este artículo . Entonces apareció un comentario sobre ella . Como resultado, profundicé en leer el material, profundicé en el debate y pude optimizar el código desde la primera parte de esta historia. Propongo caminar conmigo por los puntos principales.

Primero, quiero agradecer a Mogost . Gracias a su comentario, redefiní el enfoque de Python. Anteriormente había escuchado que había muchos tipos poco económicos entre los pitonistas (cuando se trata de memoria), pero ahora resultó que de alguna manera me uní invisiblemente a esta fiesta.

Entonces comencemos. Vamos a especular sobre cuáles fueron los cuellos de botella en general.

Persistente 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: 


Y este no es el límite. Por lo tanto, eliminé parte de los ifs, transfirí algo a __init__, es decir a donde se llamará una vez. Específicamente, la comprobación de la propiedad en el código debe llamarse una vez, porque el decorador se aplica al método y se le asigna. Y la propiedad de la clase, respectivamente, permanecerá sin cambios. Por lo tanto, no hay necesidad de verificar la propiedad constantemente.

Un punto separado es si en. El generador de perfiles mostró que cada uno de ellos tiene una llamada por separado, así que decidí combinar todos los controladores en un solo dict. Esto permitió evitar ifs en general, en lugar de usar simplemente:
 self.handlers.get(e.__class__, Exception)(e) 


así que en self.handlers tenemos un dict, que por defecto contiene una función que genera las otras excepciones.

Por supuesto, el envoltorio merece una atención especial. Esta es la misma función que se llama cada vez que se llama al decorador. Es decir aquí es mejor evitar verificaciones innecesarias y todo tipo de cargas de trabajo tanto como sea posible, poniéndolas en __init__ o __call__ si es posible. Esto es lo que antes era el contenedor:
 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) 


la cantidad de cheques pasa por el techo. Todo esto se llamará en cada llamada al decorador. Por lo tanto, el contenedor se convirtió así:
  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 


recuerde, __call__ se llamará una vez. Dentro de __call__, dependiendo del grado de asincronía de la función, devolvemos la función en sí o la rutina. Y también quiero señalar que asyncio.iscoroutinefunction hace una llamada adicional, así que cambié a inspect.iscoroutinefunction. En realidad, bancos (cProfile) para asyncio e inspeccionar:

  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) 


Y probablemente el ejemplo estaría incompleto sin tiempo. Por lo tanto, usando el ejemplo del comentario anterior:
 class MathWithTry(object): def divide(self, a, b): try: return a // b except ZeroDivisionError: return '   ,   ' 


y un ejemplo del texto del artículo anterior ( ¡ATENCIÓN! pasamos e al ejemplo del texto en el lambda. Este no fue el caso en el artículo anterior y se agregó solo en las innovaciones):
 class Math(object): @property def exception_handlers(self): return { ZeroDivisionError: lambda e: '   ,   ' } @ProcessException(exception_handlers) def divide(self, a, b): return a // b 


Aquí están los resultados para usted:
 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 conclusión, quiero decir que la optimización, en mi opinión, es un proceso bastante complicado, y aquí es importante no dejarse llevar y no optimizar algo a expensas de la legibilidad. De lo contrario, debitar en optimizado será extremadamente difícil.

Gracias por tus comentarios Espero comentarios sobre este artículo también :)

PD: gracias a los comentarios de los usuarios del Habr, fue posible acelerar aún más, eso fue lo que sucedió:
 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 en 0.03 en promedio. Gracias a Kostiantyn e Yngvie .

PD actualizado! He optimizado aún más el código basado en los comentarios de los comentarios de onegreyonewhite y resetme . Se reemplazó self.func con solo func e hizo self.handlers en una variable. La ejecución se aceleró aún más, especialmente si hay más repeticiones por cero. Cito timeit:
 timeit.timeit('t.divide_with_decorator(1, 0)', number=1000000, setup='from __main__ import t') 1.1116105649998644 


Antes de esta optimización, la ejecución con el mismo valor numérico tomó 1,24 en promedio.

PD: He optimizado aún más al introducir la función raise_exception de __init__ en @staticmethod y estoy accediendo a ella a través de una variable para eliminar el acceso a través de un punto. En realidad, el tiempo promedio de ejecución se ha convertido en:
 timeit.timeit('t.divide_with_decorator(1, 0)', number=1000000, setup='from __main__ import t') 1.0691639049982768 


Esto es por el método. Y las funciones se llaman aún más rápido (en promedio):
 timeit.timeit('div(1, 0)', number=1000000, setup='from __main__ import div') 1.0463485610016505 

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


All Articles