Python multiprocessing pool.map для декількох аргументів


534

Чи є в багатопроцесорній бібліотеці Python чи існує варіант pool.map, який підтримує кілька аргументів?

text = "test"
def harvester(text, case):
    X = case[0]
    text+ str(X)

if __name__ == '__main__':
    pool = multiprocessing.Pool(processes=6)
    case = RAW_DATASET
    pool.map(harvester(text,case),case, 1)
    pool.close()
    pool.join()

4
На мій подив, я не міг ні зробити, partialні lambdaзробити цього. Я думаю, що це стосується дивного способу передачі функцій підпроцесам (через pickle).
senderle

10
@senderle: Ця помилка в Python 2.6, але вона була виправлена ​​станом на 2.7: bugs.python.org/issue5228
unutbu

1
Просто замініть pool.map(harvester(text,case),case, 1) на: pool.apply_async(harvester(text,case),case, 1)
Tung Nguyen

3
@Syrtis_Major, будь ласка, не редагуйте питання щодо ОП, які ефективно перекосують відповіді, які раніше були надані. Додавання returnдо harvester()перетворили відповідь @senderie «s в неточною. Це не допомагає майбутнім читачам.
Ricalsin

1
Я б сказав, що легким рішенням було б упакувати всі аргументи в кортеж і розпакувати його у виконанні. Я робив це, коли мені потрібно було надіслати складні декілька аргументів у функцію, що виконується пулом процесів.
HS

Відповіді:


357

Відповідь на це залежить від версії та ситуації. Найбільш загальну відповідь на останні версії Python (починаючи з 3.3) вперше описав Дж . Ф. Себастьян . 1 Він використовує Pool.starmapметод, який приймає послідовність кортежів аргументів. Потім він автоматично розпаковує аргументи з кожного кортежу і передає їх заданій функції:

import multiprocessing
from itertools import product

def merge_names(a, b):
    return '{} & {}'.format(a, b)

if __name__ == '__main__':
    names = ['Brown', 'Wilson', 'Bartlett', 'Rivera', 'Molloy', 'Opie']
    with multiprocessing.Pool(processes=3) as pool:
        results = pool.starmap(merge_names, product(names, repeat=2))
    print(results)

# Output: ['Brown & Brown', 'Brown & Wilson', 'Brown & Bartlett', ...

Для більш ранніх версій Python вам потрібно буде написати допоміжну функцію, щоб явно розпакувати аргументи. Якщо ви хочете використовувати with, вам також потрібно написати обгортку, щоб перетворитись Poolна контекстний менеджер. (Дякую Мюону, що вказав на це.)

import multiprocessing
from itertools import product
from contextlib import contextmanager

def merge_names(a, b):
    return '{} & {}'.format(a, b)

def merge_names_unpack(args):
    return merge_names(*args)

@contextmanager
def poolcontext(*args, **kwargs):
    pool = multiprocessing.Pool(*args, **kwargs)
    yield pool
    pool.terminate()

if __name__ == '__main__':
    names = ['Brown', 'Wilson', 'Bartlett', 'Rivera', 'Molloy', 'Opie']
    with poolcontext(processes=3) as pool:
        results = pool.map(merge_names_unpack, product(names, repeat=2))
    print(results)

# Output: ['Brown & Brown', 'Brown & Wilson', 'Brown & Bartlett', ...

У більш простих випадках, з фіксованим другим аргументом, ви також можете використовувати partial, але тільки в Python 2.7+.

import multiprocessing
from functools import partial
from contextlib import contextmanager

@contextmanager
def poolcontext(*args, **kwargs):
    pool = multiprocessing.Pool(*args, **kwargs)
    yield pool
    pool.terminate()

def merge_names(a, b):
    return '{} & {}'.format(a, b)

if __name__ == '__main__':
    names = ['Brown', 'Wilson', 'Bartlett', 'Rivera', 'Molloy', 'Opie']
    with poolcontext(processes=3) as pool:
        results = pool.map(partial(merge_names, b='Sons'), names)
    print(results)

# Output: ['Brown & Sons', 'Wilson & Sons', 'Bartlett & Sons', ...

1. Багато чого з цього надихнула його відповідь, яку, напевно, слід було прийняти замість цього. Але оскільки цей застряг у верхній частині, здавалося, найкраще вдосконалити його для майбутніх читачів.


Мені здається, що RAW_DATASET в цьому випадку має бути глобальною змінною? Хоча я хочу, щоб parcial_harvester змінив значення регістру в кожному дзвінку комбайна (). Як цього досягти?
xgdgsc

Найголовніше тут - присвоєння =RAW_DATASETзначення за замовчуванням case. В іншому випадку pool.mapзаплутатимуться про кілька аргументів.
Емерсон Сюн

1
Я розгублений, що сталося зі textзмінною у вашому прикладі? Чому, RAW_DATASETздавалося б, проходять двічі. Я думаю, у вас може бути помилка друку?
Дейв

не впевнений, чому використання with .. as .. дає мені AttributeError: __exit__, але працює добре, якщо я просто зателефоную, pool = Pool();то закрийте вручну pool.close()(python2.7)
muon

1
@muon, хороший улов. Здається, Poolоб'єкти не стають менеджерами контексту до Python 3.3. Я додав просту функцію обгортки, яка повертає Poolменеджер контексту.
senderle

501

чи є варіант pool.map, який підтримує кілька аргументів?

Python 3.3 включає pool.starmap()метод :

#!/usr/bin/env python3
from functools import partial
from itertools import repeat
from multiprocessing import Pool, freeze_support

def func(a, b):
    return a + b

def main():
    a_args = [1,2,3]
    second_arg = 1
    with Pool() as pool:
        L = pool.starmap(func, [(1, 1), (2, 1), (3, 1)])
        M = pool.starmap(func, zip(a_args, repeat(second_arg)))
        N = pool.map(partial(func, b=second_arg), a_args)
        assert L == M == N

if __name__=="__main__":
    freeze_support()
    main()

Для старих версій:

#!/usr/bin/env python2
import itertools
from multiprocessing import Pool, freeze_support

def func(a, b):
    print a, b

def func_star(a_b):
    """Convert `f([1,2])` to `f(1,2)` call."""
    return func(*a_b)

def main():
    pool = Pool()
    a_args = [1,2,3]
    second_arg = 1
    pool.map(func_star, itertools.izip(a_args, itertools.repeat(second_arg)))

if __name__=="__main__":
    freeze_support()
    main()

Вихідні дані

1 1
2 1
3 1

Зверніть увагу, як itertools.izip()і itertools.repeat()як тут використовується.

Через помилку, згадану @unutbu, ви не можете використовувати functools.partial()або подібні можливості на Python 2.6, тому просту функцію обгортки func_star()слід чітко визначити. Дивіться також рішення, запропоновані авторомuptimebox .


1
F.: Ви можете розпакувати аргумент кортеж в підпису , func_starяк це: def func_star((a, b)). Звичайно, це працює лише за фіксованою кількістю аргументів, але якщо це єдиний випадок, який він має, він є більш читабельним.
Björn Pollex

1
@ Space_C0wb0y: f((a,b))синтаксис застарілий і видалений у py3k. І це тут непотрібно.
jfs

можливо більше пітонічного: func = lambda x: func(*x)замість визначення функції обгортки
dylam

1
@ zthomas.nc це питання про те, як підтримувати декілька аргументів для багатопроцесорної map.map. Якщо ви хочете знати, як викликати метод замість функції в іншому процесі Python через багатопроцесорний процес, тоді задайте окреме запитання (якщо все інше не вдасться, ви завжди можете створити глобальну функцію, яка завершує виклик методу, аналогічний func_star()вище)
jfs

1
Я б хотів, щоб були starstarmap.
Константин Ван

141

Я думаю, що нижче буде краще

def multi_run_wrapper(args):
   return add(*args)
def add(x,y):
    return x+y
if __name__ == "__main__":
    from multiprocessing import Pool
    pool = Pool(4)
    results = pool.map(multi_run_wrapper,[(1,2),(2,3),(3,4)])
    print results

вихід

[3, 5, 7]

16
Найпростіше рішення. Існує невелика оптимізація; видаліть функцію обгортки та розпакуйте argsбезпосередньо add, вона працює для будь-якої кількості аргументів:def add(args): (x,y) = args
Ахмед

1
ви можете також використовувати lambdaфункцію замість визначенняmulti_run_wrapper(..)
Андре Хольцнер

2
Хм ... насправді, використання a lambdaне працює, тому що pool.map(..)намагається підібрати задану функцію
Андре Хольцнер

Як ви користуєтеся цим, якщо бажаєте зберегти результат addу списку?
Vivek Subramanian

@Ahmed Мені подобається, як це відбувається, тому що IMHO виклик методу повинен виходити з ладу, коли число параметра не відповідає правильності.
Майкл Дорнер

56

Використання Python 3.3+ зpool.starmap():

from multiprocessing.dummy import Pool as ThreadPool 

def write(i, x):
    print(i, "---", x)

a = ["1","2","3"]
b = ["4","5","6"] 

pool = ThreadPool(2)
pool.starmap(write, zip(a,b)) 
pool.close() 
pool.join()

Результат:

1 --- 4
2 --- 5
3 --- 6

Ви також можете zip () більше аргументів, якщо вам подобається: zip(a,b,c,d,e)

Якщо ви хочете, щоб постійне значення було передано як аргумент, ви повинні використовувати, import itertoolsа потім, zip(itertools.repeat(constant), a)наприклад.


2
Це майже точна дублююча відповідь, як відповідь від @JFSebastian в 2011 році (з 60 голосами).
Майк Маккернс

29
Ні. Перш за все, він видалив багато непотрібних речей і чітко зазначає, що це для python 3.3+ і призначений для новачків, які шукають просту і чітку відповідь. Як сам початківець, знадобився певний час, щоб це зрозуміти (так, з повідомленнями JFSebastians), і саме тому я написав свій пост, щоб допомогти іншим новачкам, тому що його пост просто сказав "є стартова карта", але не пояснив цього - це це те, що має намір моя публікація. Тож немає абсолютно жодних причин бити мене двома потоками.
користувач136036

У 2011 році в python 3.3 +… не було "+", так очевидно.
Майк Маккернс,

27

Дізнавшись про itertools у відповіді Дж. Ф. Себастьяна, я вирішив зробити це на крок далі і написати parmapпакет, який піклується про паралелізацію, пропонування mapта starmapфункції на python-2.7 та python-3.2 (а також пізніше також), який може приймати будь-яку кількість позиційних аргументів .

Установка

pip install parmap

Як паралелізувати:

import parmap
# If you want to do:
y = [myfunction(x, argument1, argument2) for x in mylist]
# In parallel:
y = parmap.map(myfunction, mylist, argument1, argument2)

# If you want to do:
z = [myfunction(x, y, argument1, argument2) for (x,y) in mylist]
# In parallel:
z = parmap.starmap(myfunction, mylist, argument1, argument2)

# If you want to do:
listx = [1, 2, 3, 4, 5, 6]
listy = [2, 3, 4, 5, 6, 7]
param = 3.14
param2 = 42
listz = []
for (x, y) in zip(listx, listy):
        listz.append(myfunction(x, y, param1, param2))
# In parallel:
listz = parmap.starmap(myfunction, zip(listx, listy), param1, param2)

Я завантажив парамапу в PyPI і в сховище github .

Як приклад, на запитання можна відповісти наступним чином:

import parmap

def harvester(case, text):
    X = case[0]
    text+ str(X)

if __name__ == "__main__":
    case = RAW_DATASET  # assuming this is an iterable
    parmap.map(harvester, case, "test", chunksize=1)

19

# "Як приймати кілька аргументів".

def f1(args):
    a, b, c = args[0] , args[1] , args[2]
    return a+b+c

if __name__ == "__main__":
    import multiprocessing
    pool = multiprocessing.Pool(4) 

    result1 = pool.map(f1, [ [1,2,3] ])
    print(result1)

2
Акуратний і елегантний.
Prav001

1
Я не розумію, чому мені доводиться прокручувати всю дорогу сюди, щоб знайти найкращу відповідь.
тоті

11

Існує вилка з multiprocessingназваним патосом ( зауважте: використовуйте версію на github ), яка не потрібна starmap- функції карти відображають API для карти python, таким чином карта може приймати кілька аргументів. Завдяки цьому pathosви можете також робити багатопроцесорні перекладачі замість того, щоб застрявати в __main__блоці. Патос належить до випуску після незначного оновлення - перетворення в основному на python 3.x.

  Python 2.7.5 (default, Sep 30 2013, 20:15:49) 
  [GCC 4.2.1 (Apple Inc. build 5566)] on darwin
  Type "help", "copyright", "credits" or "license" for more information.
  >>> def func(a,b):
  ...     print a,b
  ...
  >>>
  >>> from pathos.multiprocessing import ProcessingPool    
  >>> pool = ProcessingPool(nodes=4)
  >>> pool.map(func, [1,2,3], [1,1,1])
  1 1
  2 1
  3 1
  [None, None, None]
  >>>
  >>> # also can pickle stuff like lambdas 
  >>> result = pool.map(lambda x: x**2, range(10))
  >>> result
  [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
  >>>
  >>> # also does asynchronous map
  >>> result = pool.amap(pow, [1,2,3], [4,5,6])
  >>> result.get()
  [1, 32, 729]
  >>>
  >>> # or can return a map iterator
  >>> result = pool.imap(pow, [1,2,3], [4,5,6])
  >>> result
  <processing.pool.IMapIterator object at 0x110c2ffd0>
  >>> list(result)
  [1, 32, 729]

pathosЄ декілька способів, за якими ви можете отримати точну поведінку starmap.

>>> def add(*x):
...   return sum(x)
... 
>>> x = [[1,2,3],[4,5,6]]
>>> import pathos
>>> import numpy as np
>>> # use ProcessPool's map and transposing the inputs
>>> pp = pathos.pools.ProcessPool()
>>> pp.map(add, *np.array(x).T)
[6, 15]
>>> # use ProcessPool's map and a lambda to apply the star
>>> pp.map(lambda x: add(*x), x)
[6, 15]
>>> # use a _ProcessPool, which has starmap
>>> _pp = pathos.pools._ProcessPool()
>>> _pp.starmap(add, x)
[6, 15]
>>> 

Хочу зазначити, що це не стосується структури в оригінальному питанні. [[1,2,3], [4,5,6]] розпаковується зіркою на [порох (1,2,3), порошок (4,5,6)], а не [порох (1,4) , порох (2,5), порох (3, 6)]. Якщо ви не маєте належного контролю над входами, які передаються вашій функції, вам, можливо, потрібно спочатку їх реструктурувати.
Скотт

@Scott: ах, я цього не помічав ... більше 5 років тому. Я зроблю невелике оновлення. Дякую.
Майк Маккернс

8

Ви можете використовувати наступні дві функції, щоб уникнути написання обгортки для кожної нової функції:

import itertools
from multiprocessing import Pool

def universal_worker(input_pair):
    function, args = input_pair
    return function(*args)

def pool_args(function, *args):
    return zip(itertools.repeat(function), zip(*args))

Використовуйте функцію functionзі списками аргументів arg_0, arg_1і arg_2виглядає наступним чином :

pool = Pool(n_core)
list_model = pool.map(universal_worker, pool_args(function, arg_0, arg_1, arg_2)
pool.close()
pool.join()

8

Краще рішення для python2:

from multiprocessing import Pool
def func((i, (a, b))):
    print i, a, b
    return a + b
pool = Pool(3)
pool.map(func, [(0,(1,2)), (1,(2,3)), (2,(3, 4))])

2 3 4

1 2 3

0 1 2

[]:

[3, 5, 7]


7

Ще одна проста альтернатива - загортати параметри функції в кортеж, а потім загортати параметри, які також повинні передаватися в кортежі. Це, мабуть, не ідеально під час роботи з великими фрагментами даних. Я вірю, що це буде робити копії для кожного кортежу.

from multiprocessing import Pool

def f((a,b,c,d)):
    print a,b,c,d
    return a + b + c +d

if __name__ == '__main__':
    p = Pool(10)
    data = [(i+0,i+1,i+2,i+3) for i in xrange(10)]
    print(p.map(f, data))
    p.close()
    p.join()

Дає вихід у деякому випадковому порядку:

0 1 2 3
1 2 3 4
2 3 4 5
3 4 5 6
4 5 6 7
5 6 7 8
7 8 9 10
6 7 8 9
8 9 10 11
9 10 11 12
[6, 10, 14, 18, 22, 26, 30, 34, 38, 42]

Дійсно, це все ще шукає кращого способу :(
Фабіо Діас

6

Кращий спосіб - використовувати декоратор, а не писати функцію обгортки вручну. Особливо, коли у вас є багато функцій для складання карт, декоратор заощадить ваш час, уникаючи написання обгортки для кожної функції. Зазвичай прикрашену функцію не можна вибрати, проте ми можемо використовувати її, functoolsщоб обійти її. Більше дискусій можна знайти тут .

Ось приклад

def unpack_args(func):
    from functools import wraps
    @wraps(func)
    def wrapper(args):
        if isinstance(args, dict):
            return func(**args)
        else:
            return func(*args)
    return wrapper

@unpack_args
def func(x, y):
    return x + y

Тоді ви можете зіставити це за допомогою блискавок

np, xlist, ylist = 2, range(10), range(10)
pool = Pool(np)
res = pool.map(func, zip(xlist, ylist))
pool.close()
pool.join()

Звичайно, ви завжди можете використовувати Pool.starmapв Python 3 (> = 3.3), як зазначено в інших відповідях.


Результати не такі, як очікувалося: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] Я б очікував: [0,1,2,3,4,5,6,7,8, 9,1,2,3,4,5,6,7,8,9,10,2,3,4,5,6,7,8,9,10,11, ...
Тедо Врбанець

@TedoVrbanec Результати просто повинні бути [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]. Якщо ви хочете пізніше, ви можете використовувати itertools.productзамість zip.
Сиртіс-майор

4

Інший спосіб - передати список списків до одноаргументальної програми:

import os
from multiprocessing import Pool

def task(args):
    print "PID =", os.getpid(), ", arg1 =", args[0], ", arg2 =", args[1]

pool = Pool()

pool.map(task, [
        [1,2],
        [3,4],
        [5,6],
        [7,8]
    ])

Можна, ніж побудувати список списків аргументів, улюбленим методом.


Це простий спосіб, але вам потрібно змінити свої початкові функції. Більше того, деякий час згадуються функції інших, які неможливо змінити.
WeizhongTu

Я скажу це палицею до Python zen. Має бути один і єдиний очевидний спосіб зробити це. Якщо випадково ви автор функції виклику, для цього вам слід скористатися цим методом, для інших випадків ми можемо використовувати метод imotai.
nehem

Мій вибір - використовувати кортеж, а потім негайно розгортати їх як перше в першому рядку.
nehem

3

Ось ще один спосіб зробити це, щоб IMHO був більш простим та елегантним, ніж будь-який із інших наданих відповідей.

Ця програма має функцію, яка приймає два параметри, роздруковує їх, а також друкує суму:

import multiprocessing

def main():

    with multiprocessing.Pool(10) as pool:
        params = [ (2, 2), (3, 3), (4, 4) ]
        pool.starmap(printSum, params)
    # end with

# end function

def printSum(num1, num2):
    mySum = num1 + num2
    print('num1 = ' + str(num1) + ', num2 = ' + str(num2) + ', sum = ' + str(mySum))
# end function

if __name__ == '__main__':
    main()

вихід:

num1 = 2, num2 = 2, sum = 4
num1 = 3, num2 = 3, sum = 6
num1 = 4, num2 = 4, sum = 8

Докладнішу інформацію див. У документах python:

https://docs.python.org/3/library/multiprocessing.html#module-multiprocessing.pool

Зокрема, обов'язково перевірте starmapфункцію.

Я використовую Python 3.6, я не впевнений, чи це буде працювати з більш старими версіями Python

Чому в документах немає такого прямого прикладу, як цей, я не впевнений.


2

З python 3.4.4 ви можете використовувати multiprocessing.get_context () для отримання контекстного об'єкта для використання декількох методів запуску:

import multiprocessing as mp

def foo(q, h, w):
    q.put(h + ' ' + w)
    print(h + ' ' + w)

if __name__ == '__main__':
    ctx = mp.get_context('spawn')
    q = ctx.Queue()
    p = ctx.Process(target=foo, args=(q,'hello', 'world'))
    p.start()
    print(q.get())
    p.join()

Або просто просто замінити

pool.map(harvester(text,case),case, 1)

автор:

pool.apply_async(harvester(text,case),case, 1)

2

Тут є багато відповідей, але, схоже, жоден не забезпечує сумісний код Python 2/3, який буде працювати в будь-якій версії. Якщо ви хочете, щоб ваш код просто працював , це буде працювати для будь-якої версії Python:

# For python 2/3 compatibility, define pool context manager
# to support the 'with' statement in Python 2
if sys.version_info[0] == 2:
    from contextlib import contextmanager
    @contextmanager
    def multiprocessing_context(*args, **kwargs):
        pool = multiprocessing.Pool(*args, **kwargs)
        yield pool
        pool.terminate()
else:
    multiprocessing_context = multiprocessing.Pool

Після цього ви можете використовувати мультиобробку звичайним способом Python 3, скільки завгодно. Наприклад:

def _function_to_run_for_each(x):
       return x.lower()
with multiprocessing_context(processes=3) as pool:
    results = pool.map(_function_to_run_for_each, ['Bob', 'Sue', 'Tim'])    print(results)

буде працювати в Python 2 або Python 3.


1

В офіційній документації зазначено, що він підтримує лише один ітерабельний аргумент. Мені подобається використовувати apply_async у таких випадках. У вашому випадку я би зробив:

from multiprocessing import Process, Pool, Manager

text = "test"
def harvester(text, case, q = None):
 X = case[0]
 res = text+ str(X)
 if q:
  q.put(res)
 return res


def block_until(q, results_queue, until_counter=0):
 i = 0
 while i < until_counter:
  results_queue.put(q.get())
  i+=1

if __name__ == '__main__':
 pool = multiprocessing.Pool(processes=6)
 case = RAW_DATASET
 m = Manager()
 q = m.Queue()
 results_queue = m.Queue() # when it completes results will reside in this queue
 blocking_process = Process(block_until, (q, results_queue, len(case)))
 blocking_process.start()
 for c in case:
  try:
   res = pool.apply_async(harvester, (text, case, q = None))
   res.get(timeout=0.1)
  except:
   pass
 blocking_process.join()

1
text = "test"

def unpack(args):
    return args[0](*args[1:])

def harvester(text, case):
    X = case[0]
    text+ str(X)

if __name__ == '__main__':
    pool = multiprocessing.Pool(processes=6)
    case = RAW_DATASET
    # args is a list of tuples 
    # with the function to execute as the first item in each tuple
    args = [(harvester, text, c) for c in case]
    # doing it this way, we can pass any function
    # and we don't need to define a wrapper for each different function
    # if we need to use more than one
    pool.map(unpack, args)
    pool.close()
    pool.join()

1

Це приклад рутини, яку я використовую для передачі декількох аргументів функції з одним аргументом, використовуваною у вилці pool.imap :

from multiprocessing import Pool

# Wrapper of the function to map:
class makefun:
    def __init__(self, var2):
        self.var2 = var2
    def fun(self, i):
        var2 = self.var2
        return var1[i] + var2

# Couple of variables for the example:
var1 = [1, 2, 3, 5, 6, 7, 8]
var2 = [9, 10, 11, 12]

# Open the pool:
pool = Pool(processes=2)

# Wrapper loop
for j in range(len(var2)):
    # Obtain the function to map
    pool_fun = makefun(var2[j]).fun

    # Fork loop
    for i, value in enumerate(pool.imap(pool_fun, range(len(var1))), 0):
        print(var1[i], '+' ,var2[j], '=', value)

# Close the pool
pool.close()

-3

для python2 ви можете використовувати цей трюк

def fun(a,b):
    return a+b

pool = multiprocessing.Pool(processes=6)
b=233
pool.map(lambda x:fun(x,b),range(1000))

чому b = 233. перемагає мету питання
як, якби
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.