Comments 27
Есть подозрение, что эта строчка делает не то, что от нее ожидают.
self.handlers.get(e.__class__, Exception)(e)
Второй параметр — это значение по умолчанию, а не ключ по умолчанию. Вместо вызова raise_exception
просто вернётся Exception
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)
...
В новых питонах еще есть partialmethod, но идея такая же.
Странно вы все же измеряете. Вот так у меня:
$ 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
$ 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 передаешь, там больше выигрыш.
Если запускать 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
функций.
В 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'
В принципе всю эту магию можно было и в во врапер запихнуть.
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, но создавать по методу на каждую декорацию мне кажется расточительным по памяти и немного нагромождённым для одного метода.
Во-первых, wrapper тоже не async, а значит "мне не нужен async call" — решаемая проблема.
Во-вторых, оберните только async: для чего оборачивать обычный метод?
Python v3.x: как увеличить скорость декоратора без регистрации и смс