Pythonで並列処理(Pool)を用いて実行した関数から複数の戻り値を受け取るには

## はじめに
Pythonで並列処理を行う際、以下のケースでどう書くのか戸惑ったので、備忘録としてまとめました。
* multiprocessing.Poolを使用
* mapメソッドを使用
* 呼び出し先の関数に複数の戻り値がある
* これら戻り値を受け取って何らかの処理をしたい

## 使用環境
* OS  - macOS Catalina 10.15.7
* CPU - Intel(R) Core(TM) i7-7567U CPU @ 3.50GHz
* RAM - 16 GB 2133 MHz LPDDR3
* Python 3.8.6

## 実装内容
今回は、例として下記のようなサンプルコードを作ってみました。
1. 2つの値を渡すと、最大公約数と最小公倍数を求めてそれぞれを返却する関数を用意
2. 並列なしと並列ありでそれぞれ呼び出してみる

## ソースコード
### 並列なし
こちらは`NUMBERS`というリストの長さだけ単純に関数を呼び出しています。

```python:
import time

def calc(num_pair):
    x, y = num_pair
    low = min(x, y)
    gcd = 0

    # Greatest common divisor
    for i in range(low, 0, -1):
        if x % i == 0 and y % i == 0:
            gcd = i
            break
    
    # Least common multiple
    lcm = x * y // gcd

    return gcd, lcm


if __name__ == "__main__":

    result=[]
    NUMBERS = [
        (12345678,24680246),(91845010,35889830),
        (82163657,75546871),(46015383,43872681),
        (73739990,64003993),(26514146,33211514),
        (51395783,78597259),(99939535,88084461)
    ]
    start = time.time()

    gcd = []
    lcm = []
    for  i, pair in enumerate(NUMBERS):
        g, l = calc(NUMBERS[i])
        gcd.append(g)
        lcm.append(l)

    print(f'gcd = {gcd}')
    # gcd = [2, 10, 1, 3, 1, 2, 1, 1]
    print(f'lcm = {lcm}')
    # lcm = [152347185038394, 329630179524830, 6207207196267247, 672939406483941, 4719653803780070, 440287465538522, 4039567667958797, 8803120073065635]

    for i, pair in enumerate(NUMBERS):
        print(f'{pair[0]}と{pair[1]}の最大公約数は{gcd[i]}, 最小公倍数は{lcm[i]}です')
    # 出力結果は「並列あり」にて

    end = time.time()
    print(f'Time = {(end-start):.3f}')
    # 3回計測した結果
    # Time = 29.068
    # Time = 29.796
    # Time = 29.890
```

### 並列あり
ポイントは`p.map()`で受け取ったresultの中身です。
関数の戻り値が増えるほど、resultの1つのタプルに入る数が増える、と分かればなんてことないのですが、知らないと並列なしで書いたような値の受け取り方をしたくなりますよね。

```python
from multiprocessing import Pool
import time

def calc(num_pair):

    x, y = num_pair
    low = min(x, y)
    gcd = 0

    # Greatest common divisor
    for i in range(low, 0, -1):
        if x % i == 0 and y % i == 0:
            gcd = i
            break
    
    # Least common multiple
    lcm = x * y // gcd

    return gcd, lcm


if __name__ == "__main__":

    result=[]
    NUMBERS = [
        (12345678,24680246),(91845010,35889830),
        (82163657,75546871),(46015383,43872681),
        (73739990,64003993),(26514146,33211514),
        (51395783,78597259),(99939535,88084461)
    ]
    start = time.time()
    
    p = Pool(8)

    try:
        result = p.map(calc, NUMBERS)
    except Exception as e:
        print(e)

    print(f'result = {result}')
    # result = [(2, 152347185038394), (10, 329630179524830), (1, 6207207196267247), (3, 672939406483941), (1, 4719653803780070), (2, 440287465538522), (1, 4039567667958797), (1, 8803120073065635)]

    gcd = [i[0] for i in result]
    lcm = [j[1] for j in result]


    print(f'gcd = {gcd}')
    # gcd = [2, 10, 1, 3, 1, 2, 1, 1]
    print(f'lcm = {lcm}')
    # lcm = [152347185038394, 329630179524830, 6207207196267247, 672939406483941, 4719653803780070, 440287465538522, 4039567667958797, 8803120073065635]

    for i, pair in enumerate(NUMBERS):
        print(f'{pair[0]}と{pair[1]}の最大公約数は{gcd[i]}, 最小公倍数は{lcm[i]}です')
    # 12345678と24680246の最大公約数は2, 最小公倍数は152347185038394です
    # 91845010と35889830の最大公約数は10, 最小公倍数は329630179524830です
    # 82163657と75546871の最大公約数は1, 最小公倍数は6207207196267247です
    # 46015383と43872681の最大公約数は3, 最小公倍数は672939406483941です
    # 73739990と64003993の最大公約数は1, 最小公倍数は4719653803780070です
    # 26514146と33211514の最大公約数は2, 最小公倍数は440287465538522です
    # 51395783と78597259の最大公約数は1, 最小公倍数は4039567667958797です
    # 99939535と88084461の最大公約数は1, 最小公倍数は8803120073065635です

    end = time.time()
    print(f'Time = {(end-start):.3f}')
    # 3回計測した結果
    # Time = 18.861
    # Time = 16.983
    # Time = 18.362
```
## 結論
Poolとmapで複数の値を返却する関数を呼ぶと、返却数分タプルで格納された状態のリストで返却される。

## 補足
今回は説明のためにわざと時間のかかる処理にしようと上記のようなコードにしましたが、本当に速く最大公約数を求めたい方は以下のように既存のライブラリ使った方が良いでしょう。

```python
import math
from multiprocessing import Pool
import time

def calc(num_pair):
    x, y = num_pair

    # Greatest common divisor
    gcd = math.gcd(x,y)
    
    # Least common multiple
    lcm = x * y // gcd

    return gcd, lcm


if __name__ == "__main__":
    result=[]
    NUMBERS = [
        (12345678,24680246),(91845010,35889830),
        (82163657,75546871),(46015383,43872681),
        (73739990,64003993),(26514146,33211514),
        (51395783,78597259),(99939535,88084461)
    ]
    start = time.time()

    gcd = []
    lcm = []
    for  i, pair in enumerate(NUMBERS):
        g, l = calc(NUMBERS[i])
        gcd.append(g)
        lcm.append(l)

    for i, pair in enumerate(NUMBERS):
        print(f'{pair[0]}と{pair[1]}の最大公約数は{gcd[i]}, 最小公倍数は{lcm[i]}です')

    end = time.time()
    print(f'Time = {(end-start):.5f}')
    # Time = 0.00013
```

`math.gcd()`を使ったほうが断然速いです。
こちらで並列処理にすると、なぜか`Time = 0.17145`と並列なしの方が速く終えました。

なかなか並列処理も奥深いものですね。