Pull to refresh

Comments 27

Есть подозрение, что эта строчка делает не то, что от нее ожидают.


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

Второй параметр — это значение по умолчанию, а не ключ по умолчанию. Вместо вызова raise_exception просто вернётся Exception

Еще можно слегка ускорить используя partial:
from functools import partial
...
    def __call__(self, func):
        self.func = func

        if iscoroutinefunction(self.func):
            return partial(self._coroutine_exception_handler, self)
        return partial(self._sync_exception_handler, self)
...
возможно, я что-то делаю не так, но у меня timeit показывает одинаковое время для моего и вашего варианта. А еще ваше решение справедливо только для методов, но не для функций.
Еще нолик добавьте к количеству лупов. В примере маловато для быстрых машин. Еще может быть Python не собран с С модулем functools, а в коде есть файлбэк на питоновскую версию. Пропробуйте симортировать модуль _functools. Если импортируется, то все ОК.

В новых питонах еще есть partialmethod, но идея такая же.
_functools импортируется. Но ваш пример не работает для функций, потому что там self нету
А метод разве не функция которой передается self первым параметром?
да, метод — это функция, принадлежащая классу. Поэтому и разделяют понятия функция и метод. Для функций ваш код не работает, потому что вы пытаетесь в них тоже передавать self. Таким образом, передается на аргумент больше и рейзится TypeError.
а, кстати, почему partial ускоряет выполнение? я попытался разобраться, только ни к чему не пришел пока что.
self как бы вмораживается в вызов и на него не таратится время на его передачу в стек, а вся машинерия на С, от суда есть прирост но незначительный. А лучше вообще не вызывать, как в соседней ветку.

Странно вы все же измеряете. Вот так у меня:
$ python3 -m timeit -s 'from test import math_with_try' 'math_with_try.divide(1,0)'
1000000 loops, best of 3: 0.415 usec per loop
$ python3 -m timeit -s 'from test import math_with_decorator' 'math_with_decorator.divide(1,0)'
1000000 loops, best of 3: 1.08 usec per loop
$ python3 -m timeit -s 'from test import math_with_partial' 'math_with_partial.divide(1,0)'
1000000 loops, best of 3: 0.998 usec per loop
благодарю за объяснение, partial — полезно учесть для работы с методами. Но если я правильно понял, то для функций эта оптимизация не имеет значения?
Небольшой прирост будет:
$ python3 -m timeit -s 'from test1 import divide_decorator' 'divide_decorator(1,0)'                       
1000000 loops, best of 3: 1.07 usec per loop
$ python3 -m timeit -s 'from test1 import divide_partial' 'divide_partial(1,0)'                           
1000000 loops, best of 3: 0.993 usec per loop

Только вызов как-то вот так нужно сделать:
    def __call__(self, func):
        if iscoroutinefunction(func):
            return partial(self._coroutine_exception_handler, func)
        return partial(self._sync_exception_handler, func)

    def _sync_exception_handler(self, func, *args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            return self.handlers.get(e.__class__, Exception)(e)

Получается так чем больше аргументов в partial передаешь, там больше выигрыш.
спасибо за пример. На текущий момент код был слегка отредактирован и оптимизирован, я в конце статьи добавил обновленный пример и учел кое-что из ваших замечаний. Возможно, в коде выше тоже нет предела совершенству?
Вы все же некорректно измеряете. Вам функция возвращает время выполнения цикла 100000 итераций. Во время выполнения на машине могут произойти прерывания и интрепретатор Python может получить меньший квант времени, а это скажется на времени исполнения этого цикла.

Если запускать timeit из командной строки, то выводится лучшее время из 3 запусков цикла. Это позволяет нивелировать прерывания во время исполнения, так как лучшее время исполнения даст та итерация во время которой произойдет наименьшее количество прерываний.

Обпатите внимание на это предложение в документации docs.python.org/3/library/timeit.html
Note however that timeit() will automatically determine the number of repetitions only when the command-line interface is used.

Другая проблема в самом декораторе. По сути он замалчивает исключения по таймауту и исключения от асинхронной очереди. Что в корне неправильно. Они должны быть обработаны и желательно не в декораторе, а в коде метода или функции. Иначе ваш декоратор может привести к ситуации когда замолченное исключение будет годами прятать баг. Лучше не замалчивать, а дать упасть приложению, а по трейсбеку найти проблему и пофиксить.

Сейчас в мире Python получилось так что мы имеем два Python: один асинхронный, а другой синхронный. Они в принципе разные и лучше код для разных питонов держать отдельно. Иначе выходят вот такие библиотеки, которые позволяют любым путем, но выполнить функцию будь она асинхронная или синхронная: github.com/miyakogi/syncer/blob/master/syncer.py

Да и просто расставив sync/await не получить правильно работающий асинхронный код. Если убрать асинхронность из декоратора, то вообщем ничего не изменится для вызываемого под ним метода или функции. По существу обработка исключений — это не IO bound задача для которой придумали асинхронность, поэтому нет смысла обрабатывать исключения асинхронно, так как в момент обработки может прилететь какое-то совсем другое исключение и совсем из другого места кода.

Для обучения это декоратор хороший пример и мне понравился Ваш подход к постоянному улучшению. Да и обе статьи получились интересные.

Другая проблема в самом декораторе. По сути он замалчивает исключения по таймауту и исключения от асинхронной очереди.


не, тут все правильно. Это специфика моего проекта. Там сервер в инфинити лупе ожидает запросов с клиента и отвечает. А если ничего не пришло, возникает TimeoutError. Есть ряд исключений, которые мне приходится игнорировать, потому что они происходят в цикле. Постоянно. Но это конкретно мой проект. А в общем, конечно, лучше убрать.

P.S. благодарю за положительную оценку статей

У Вас ещё wrapper дополнительно вызывает другой метод — что в Python весьма не бесплатно.
Т. е. содержимое методов _coroutine_exception_handler и _sync_exception_handler по хорошему бы переместить внутрь объявления самих wrapper функций.

обожаю хабр :) да, это очень важный момент, выполняться стало аж на 0.03 быстрее, добавлю в статью (и в свой код). Благодарю за замечание
ИМХО Высокопроизводительные задачи лучше перекладывать на C(++) или другие производительные языки(Rust, Go(в некоторых задачах)).
В python что что а оптимизации кот наплакал и результат чаще всего не превышает пары десятков процентов, та и в целом язык отстаёт по скорости от С-шных собратьев.
Для задач автоматизации по типу парсинга и обработки данных он отлично подходит(scrapy, bs, lxml). Читаемость кода, скорость и асинхронность здесь на хорошем уровне. В случае если нужна производительность лично предпочёл бы C.

Здесь явно не то что вы хотите:


custom_handlers = custom_handlers.__get__(self, self.__class__)

Нужно использовать дескрипторы чтобы получить правильный self

Пример (парсер ест двойные пустые строки):


import types
from asyncio import QueueEmpty, QueueFull
from concurrent.futures import TimeoutError
from functools import wraps
from inspect import iscoroutinefunction

class ProcessException:
    __slots__ = ('func', 'custom_handlers')

    exclude = {
        QueueEmpty: lambda e: None,
        QueueFull: lambda e: None,
        TimeoutError: lambda e: None
    }

    def __init__(self, custom_handlers=None):
        self.func = None
        self.custom_handlers = custom_handlers

    def __call__(self, func):
        self.func = func

        if isinstance(self.custom_handlers, property):
            return self

        return self._get_wrapper()

    def __get__(self, instance, owner):
        setattr(owner, self.func.__name__, self._get_wrapper(instance, owner))

        return getattr(instance, self.func.__name__)  # return bounded method

    def _get_wrapper(self, instance=None, owner=None):
        if isinstance(self.custom_handlers, property):
            self.custom_handlers = self.custom_handlers.__get__(instance, owner)

        handlers = {
            **self.exclude,
            **(self.custom_handlers or {}),
            Exception: self._raise_exception
        }
        del self.custom_handlers

        if iscoroutinefunction(self.func):
            async def wrapper(*args, **kwargs):
                try:
                    return await self.func(*args, **kwargs)
                except Exception as e:
                    return handlers.get(type(e), handlers[Exception])(e)

        else:
            def wrapper(*args, **kwargs):
                try:
                    return self.func(*args, **kwargs)
                except Exception as e:
                    return handlers.get(type(e), handlers[Exception])(e)

        return wraps(self.func)(wrapper)

    @staticmethod
    def _raise_exception(e: Exception):
        raise e

class Math(object):
    def __init__(self, error_message: str = 'Cannot divide by zero!'):
        self.error_message = error_message

    @property
    def exception_handlers(self):
        return {
            ZeroDivisionError: lambda e: self.error_message
        }

    @ProcessException(exception_handlers)
    def divide(self, a, b):
        return a // b

@ProcessException({ZeroDivisionError: lambda e: 'Cannot divide by zero!'})
def divide(a, b):
    return a // b

assert Math().divide(4, 2) == 2
assert divide(4, 2) == 2
assert Math().divide(4, 0) == 'Cannot divide by zero!'
assert divide(4, 0) == 'Cannot divide by zero!'
assert Math().divide.__name__ == 'divide'

В принципе всю эту магию можно было и в во врапер запихнуть.

я пытаюсь получить значение property, оно вернется даже если я передам object.__class__:
custom_handlers = custom_handlers.__get__(self, object.__class__)


если вы считаете, что такое поведение неправильное и может привести к багу — можете ли вы привести шаги воспроизведения этого бага?

Проблема будет при обращении к self внутри проперти:


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

class Math(object):
    def __init__(self, error_message: str = 'Cannot divide by zero!'):
        self.error_message = error_message

    @property
    def exception_handlers(self):
        return {
            ZeroDivisionError: lambda e: self.error_message
        }

    @ProcessException(exception_handlers)
    def divide(self, a, b):
        return a // b

assert Math().divide(4, 2) == 2
assert Math().divide(4, 0) == 'Cannot divide by zero!'
assert Math().divide.__name__ == 'divide'

AttributeError: 'ProcessException' object has no attribute 'error_message'
class ProcessException:
     ...
     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

В этом месте вообще не вижу смысла в том, чтобы каждый раз возвращать wrapper и ещё и создавать его. Почему бы сразу не возвращать метод объекта?
Более того, каждый раз когда вызывается self., то Python ищет этот атрибут в словаре атрибутов. Если возвращать сразу метод, то это минус один сложный вызов (внутри wrapper'а).
Либо, как предложил ADR, логику запихнуть во wrapper, но создавать по методу на каждую декорацию мне кажется расточительным по памяти и немного нагромождённым для одного метода.

мы не можем сразу возвращать метод объекта, потому что метод может быть с async, а значит, придется его дождаться (await), а уже потом обернуть в try except. Но чтобы дождаться, функция-контейнер (в нашем случае, __call__) тоже должна быть async. Со всеми отсюда вытекающими. Мне не нужен async __call__.

Во-первых, wrapper тоже не async, а значит "мне не нужен async call" — решаемая проблема.
Во-вторых, оберните только async: для чего оборачивать обычный метод?

для того, чтобы вернуть обработчик исключения (если таковой есть). В секции except тоже происходит вызов функции и в сценарии с wrapper-ом она вполне вписывается. Не очень представляю, как можно выполнить обработчик, если мы не будем оборачивать во wrapper. Я, кстати, на основе комментариев выше кое-что еще оптимизировал. Поубирал self и получилось еще быстрее — особенно заметно, когда лупов на нолик больше, чем в примере выше. Т.е. ранее было 1.24, стало — 1.08 секунды для 1000000 повторов. Сейчас добавлю в статью обновленный код.
Я еще больше оптимизировал. Вынес в @staticmethod из __init__ функцию raise_exception и через переменную уже к ней обращаюсь (чтобы убрать вызов через точку). В итоге среднее выполнение для 1000000 повторов стало 1.06. Сейчас отредактирую пример кода выше.
Sign up to leave a comment.

Articles