Хабрахабр

Python v3.x: как увеличить скорость декоратора без регистрации и смс

Вначале была эта статья. Потом к ней появился комментарий. А в результате я углубился в чтение матчасти, закопался в дебаг и смог оптимизировать код из первой части этой истории. Предлагаю вместе со мной пройтись по основным моментам.

Благодаря его комментарию я пересмотрел подход к Пайтону. Для начала хочу поблагодарить Mogost. Я и ранее слыхал о том, что среди пайтонистов достаточно много неэкономных ребят (при обращении с памятью), а теперь выяснилось, что я как-то незаметно для себя присоединился к этой тусовке.

Давайте порассуждаем, а какие вообще были узкие места. Итак, начнем.

Постоянные if:

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

Поэтому часть if-ов я убрал, кое-что перенес в __init__, т.е. и это не предел. Конкретно проверка на property в коде должна быть вызвана единоразово, т.к. туда, где это будет вызвано один раз. И property класса, соответственно, останется неизменным. декоратор применяется к методу и закрепляется за ним. Поэтому и незачем проверять property постоянно.

Профайлер показал, что на каждый такой in отдельный вызов, поэтому я решил все хэндлеры объединить в один dict. Отдельный момент это if in. Это позволило избежать if-ов вообще, взамен используя просто:

self.handlers.get(e.__class__, Exception)(e)

таким образом в self.handlers у нас находится dict, который в качестве значения по умолчанию содержит функцию, рейзящую остальные исключения.

Это та самая функция, которая вызывается каждый раз, когда вызывается декоратор. Отдельного внимания, конечно же, заслуживает wrapper. здесь лучше по максимуму избежать лишних проверок и всяких нагрузок, по возможности вынеся их в __init__ или в __call__. Т.е. Вот какой wrapper был ранее:

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)

Это все будет вызываться на каждом вызове декоратора. количество проверок зашкаливает. Поэтому wrapper стал таким:

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

Внутри __call__ мы в зависимости от степени асинхронности функции возвращаем саму функцию или корутин. напомню, __call__ будет вызван один раз. Собственно, бенчи (cProfile) для asyncio и inspect: И дополнительно хочу заметить, что asyncio.iscoroutinefunction делает дополнительный вызов, поэтому я перешел на inspect.iscoroutinefunction.

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 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}

Полный код:

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)

Поэтому используя пример из вышеупомянутого комментария:
И наверное, пример был бы неполным без timeit.

class MathWithTry(object): def divide(self, a, b): try: return a // b except ZeroDivisionError: return 'Делить на ноль нельзя, но можно умножить'

В предыдущей статье этого не было и добавилось только в нововведениях):
и пример из текста предыдущей статьи (ВНИМАНИЕ! в пример из текста в лямбду мы передаем e.

class Math(object): @property def exception_handlers(self): return { ZeroDivisionError: lambda <b>e</b>: 'Делить на ноль нельзя, но можно умножить' } @ProcessException(exception_handlers) def divide(self, a, b): return a // b

вот вам результаты:

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

Иначе дебажить по оптимизированному будет крайне сложно. В качестве заключения хочу сказать, что оптимизация, на мой взгляд, процесс достаточно непростой и тут важно не увлечься и не оптимизировать что-то в ущерб читаемости.

Жду комментариев и к этой статье тоже 🙂 Благодарю за ваши комментарии.

S. P. благодаря замечаниям пользователей хабра удалось еще больше ускорить, вот, что получилось:

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): async def wrapper(*args, **kwargs): try: return await self.func(*args, **kwargs) except Exception as e: return self.handlers.get(e.__class__, self.handlers[Exception])(e) else: def wrapper(*args, **kwargs): try: return self.func(*args, **kwargs) except Exception as e: return self.handlers.get(e.__class__, self.handlers[Exception])(e) return wrapper

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

03 в среднем. Ускорилось на 0. Спасибо Kostiantyn и Yngvie.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть