Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Вначале была эта статья. Потом к ней появился комментарий. А в результате я углубился в чтение матчасти, закопался в дебаг и смог оптимизировать код из первой части этой истории. Предлагаю вместе со мной пройтись по основным моментам.
Для начала хочу поблагодарить Mogost. Благодаря его комментарию я пересмотрел подход к Пайтону. Я и ранее слыхал о том, что среди пайтонистов достаточно много неэкономных ребят (при обращении с памятью), а теперь выяснилось, что я как-то незаметно для себя присоединился к этой тусовке.
Итак, начнем. Давайте порассуждаем, а какие вообще были узкие места.
Постоянные if:
и это не предел. Поэтому часть if-ов я убрал, кое-что перенес в __init__, т.е. туда, где это будет вызвано один раз. Конкретно проверка на property в коде должна быть вызвана единоразово, т.к. декоратор применяется к методу и закрепляется за ним. И property класса, соответственно, останется неизменным. Поэтому и незачем проверять property постоянно.
Отдельный момент это if in. Профайлер показал, что на каждый такой in отдельный вызов, поэтому я решил все хэндлеры объединить в один dict. Это позволило избежать if-ов вообще, взамен используя просто:
таким образом в self.handlers у нас находится dict, который в качестве значения по умолчанию содержит функцию, рейзящую остальные исключения.
Отдельного внимания, конечно же, заслуживает wrapper. Это та самая функция, которая вызывается каждый раз, когда вызывается декоратор. Т.е. здесь лучше по максимуму избежать лишних проверок и всяких нагрузок, по возможности вынеся их в __init__ или в __call__. Вот какой wrapper был ранее:
количество проверок зашкаливает. Это все будет вызываться на каждом вызове декоратора. Поэтому wrapper стал таким:
напомню, __call__ будет вызван один раз. Внутри __call__ мы в зависимости от степени асинхронности функции возвращаем саму функцию или корутин. И дополнительно хочу заметить, что asyncio.iscoroutinefunction делает дополнительный вызов, поэтому я перешел на inspect.iscoroutinefunction. Собственно, бенчи (cProfile) для asyncio и inspect:
Полный код:
И наверное, пример был бы неполным без timeit. Поэтому используя пример из вышеупомянутого комментария:
и пример из текста предыдущей статьи (ВНИМАНИЕ! в пример из текста в лямбду мы передаем e. В предыдущей статье этого не было и добавилось только в нововведениях):
вот вам результаты:
В качестве заключения хочу сказать, что оптимизация, на мой взгляд, процесс достаточно непростой и тут важно не увлечься и не оптимизировать что-то в ущерб читаемости. Иначе дебажить по оптимизированному будет крайне сложно.
Благодарю за ваши комментарии. Жду комментариев и к этой статье тоже :)
Для начала хочу поблагодарить 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
В качестве заключения хочу сказать, что оптимизация, на мой взгляд, процесс достаточно непростой и тут важно не увлечься и не оптимизировать что-то в ущерб читаемости. Иначе дебажить по оптимизированному будет крайне сложно.
Благодарю за ваши комментарии. Жду комментариев и к этой статье тоже :)