Описание проблемы
TL; DR
Как реализовать декораторы, не нарушающие процесс засолки ?
Цель
multiprocessing.Pool
может использоваться для разделения данных и их распределения по процессам для распараллеливания данных данной функции. Я хотел бы использовать такой подход в декораторе для удобного параллелизации данных. Декоратор обычно будет выглядеть следующим образом:
from multiprocessing import Pool
from functools import partial, wraps
def deco_data_parallel(func):
@wraps(func)
def to_parallel(arg, **kwargs):
part_func = partial(func, **kwargs)
tot = 0
with Pool() as p:
for output in p.imap_unordered(part_func, arg):
tot += output
return tot
return to_parallel
Вышеуказанная реализация налагает следующие условия на функцию, которая должна быть распараллелена. Скорее всего, эти ограничения можно преодолеть с помощью лучшего дизайна.
arg
- это итерация, которая может быть разбита на куски - Аргументы исправления должны называться аргументами ключевых слов
Вот пример предполагаемого использования:
@deco_data_parallel
def compute(data, arg1, arg2):
return data + arg1 + arg2
if __name__ == "__main__":
# Dummy data
data = [4]*100000
# Fix arguments must be used as keyword arguments
compute(data, arg1=1, arg2=2)
Ошибка
Функция, поданная на imap_unordered
, должна быть выбираемой. Декоратор, кажется, нарушает возможность выбора исходной функции:
_pickle.PicklingError: Can't pickle <function compute at 0x1040137a0>: it's not the same object as __main__.compute
Лучшее решение
Сначала я подумал, что проблема была @wraps
: если декорированная функция идентична исходной функции тогда последний не может быть найден по бассейнам. Но оказывается, что декоратор @wraps
не имеет никакого эффекта.
Благодаря этой замечательной записи я мог бы предложить следующее неоптимальное решение: создать новое Функциональный объект верхнего уровня с использованием декоратора явно следующим образом. Это частично нарушает удобство для пользователя и, следовательно, не удовлетворяет. Тем не менее, он выполняет ожидаемую цель.
# Beware: the names should not be the same
compute_ = deco_data_parallel(compute)
if __name__ == "__main__":
...
compute_(data, arg1=1, arg2=2)
Вопросы
- Как элегантно решить проблему выбора, чтобы пользователь мог просто украсить функцию, которая будет распараллелена?
- Почему декоратор
@functools.wraps
не имеет никакого эффекта? - Что на самом деле означает ошибка
it's not the same object as __main__.compute
? Т.е. в каком именно смысле я нарушаю процесс травления?
Моя конфигурация
Macport Python 3.7.7 на OSX 10.14.6
Отказ от ответственности
Я довольно новичок в мире параллельных вычислений в python, а также в мире python декораторов. Ересь, скорее всего, произошла в этом посте, и я прошу прощения за это!
Это также мой второй вопрос по StackOverflow, приветствуются любые предложения по улучшению.
Подробно расследование
Убедившись, что это сработает, я попробовал несколько стратегий декорирования . Этот очень полный пост по вопросу декораторов был отличным руководством. Это сообщение в блоге дало мне некоторую надежду, что объектно-ориентированная стратегия украшения может заставить вещи работать: автор действительно утверждает, что она исправила его / ее проблему выбора.
Все следующие подходы имеют были протестированы, с @wraps
и без него, и все они приводят к одной и той же _pickle.PicklingError
ошибке. У меня появляется ощущение, что я испробовал все нехакерские возможности, которые python предлагает, и было бы очень приятно оказаться неправым!
Функциональный подход
Наиболее простой подход - тот, который я показал выше. Для декораторов с аргументами также можно использовать «фабрику декораторов». Давайте для примера рассмотрим количество процессоров.
def factory_data_parallel(nproc=4):
def deco_data_parallel(func):
@wraps(func)
def to_parallel(arg, **kwargs):
part_func = partial(func, **kwargs)
tot = 0
with Pool(nproc) as p:
for output in p.imap_unordered(part_func, arg):
tot += output
return tot
return to_parallel
return deco_data_parallel
# Usage: only with the argument (or parenthesis at least)
@factory_data_parallel(8)
def compute(data, arg1, arg2):
...
Гибридная форма, которую можно использовать в качестве простого декоратора или фабрики декораторов, будет реализована следующим образом:
def factorydeco_data_parallel(_func=None, *, nproc=4):
def deco_data_parallel(func):
...
if _func is None:
return deco_data_parallel
else:
return deco_data_parallel(_func)
# Usage as a factory (with argument)
@factorydeco_data_parallel(8)
def compute(data, arg1, arg2):
...
# Usage as a simple decorator
@factorydeco_data_parallel
def other_compute(data, arg1, arg2):
...
Объектно-ориентированный подход
Насколько я понимаю, декоратором может быть любой вызываемый объект. Простой декоратор с использованием объекта может быть реализован следующим образом. Первая версия вызывается с круглыми скобками (явное создание объекта при оформлении), а вторая используется в качестве стандартного декоратора.
class Class_data_parallel(object):
def __call__(self, func):
self.orig_func = func
@wraps(func)
def to_parallel(arg, **kwargs):
# Does it make a difference to use the argument func instead?
part_func = partial(self.orig_func, **kwargs)
tot = 0
with Pool() as p:
for output in p.imap_unordered(part_func, arg):
tot += output
return tot
return to_parallel
class Class_data_parallel_alt(object):
def __init__(self, func):
self.orig_func = func
# PB: no way I'm aware of to use @wraps
def __call__(self, arg, **kwargs):
part_func = partial(self.orig_func, **kwargs)
tot = 0
with Pool() as p:
for output in p.imap_unordered(part_func, arg):
tot += output
return tot
# Usage: with parenthesis
@Class_data_parallel()
def compute(data, arg1, arg2):
...
# Usage: without parenthesis
@Class_data_parallel_alt
def other_compute(data, arg1, arg2):
...
Очевидное расширение первого случая может позволить добавить некоторые параметры конструктору. Затем класс будет играть роль декоратора.
Еще немного мыслей
- Как я уже говорил,
@wraps
был кандидатом как для причины, так и для решения проблемы. Использование этого или нет ничего не меняет - Использование
parallel
для обработки константных аргументов (т. Е. Констант в разных процессах, arg1
и arg2
в моих примерах) может быть проблемой, но я сомневаюсь. Я мог бы использовать аргумент initializer
конструктора Pool()
. - A. Шерман и П. Ден Хартог достигли этой цели в своей параллельной модели DECO . Однако я не могу понять, как они преодолели мою проблему. Кажется, это доказывает, что то, что я хочу сделать, не является фундаментальным ограничением декораторов.