02.11.2019       Выпуск 306 (28.10.2019 - 03.11.2019)       Статьи

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

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

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

Читать>>




Экспериментальная функция:

Ниже вы видите текст статьи по ссылке. По нему можно быстро понять ссылка достойна прочтения или нет

Просим обратить внимание, что текст по ссылке и здесь может не совпадать.

Вначале была

эта статья

. Потом к ней появился

комментарий

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

Для начала хочу поблагодарить

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 постоянно.

Отдельный момент это if in. Профайлер показал, что на каждый такой in отдельный вызов, поэтому я решил все хэндлеры объединить в один dict. Это позволило избежать 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__ мы в зависимости от степени асинхронности функции возвращаем саму функцию или корутин. И дополнительно хочу заметить, что asyncio.iscoroutinefunction делает дополнительный вызов, поэтому я перешел на inspect.iscoroutinefunction. Собственно, бенчи (cProfile) для asyncio и inspect:

   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}

Полный код:

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

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

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

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

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

Ускорилось на 0.03 в среднем. Спасибо

Kostiantyn

и

Yngvie

.






Разместим вашу рекламу

Пиши: mail@pythondigest.ru

Нашли опечатку?

Выделите фрагмент и отправьте нажатием Ctrl+Enter.

Система Orphus