Pythonのreduceでちょっと遊ぶ

会社の人が整えてくれたPythonトレーニング資料の以下演習問題を考えてみる。
演習問題2:次のようなリストが与えられたときに、偶数のみを累積和するような処理を高階関数reduceを使って実装せよ。
sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
の場合、以下が出力される。
[0, 2, 2, 6, 6, 12, 12, 20, 20, 30]
なお無名関数での実装は難しいと思われるので、別途関数を定義し、reduceに処理させても良い。
無名関数での実装は難しいと思われる、らしいので無名関数で実装してみることにした。

動きとしては全てのlistの要素を見て、条件に該当するもの (今回は偶数であることが条件なので n % 2 == 0
で引っかけられそう)は累積和を実行する。
1:偶数でないので初期値0に何も実行しない→0
2:偶数なので初期値0に2を足す→2
3:偶数でないので2に何も実行しない→2
4:偶数なので2に4を足す→6
5:偶数でないので6に何も実行しない→6
6:偶数なので6に6を足す→12
7:偶数でないので12に何も実行しない→12
8:偶数なので12に8を足す→20
9:偶数でないので20に何も実行しない→20
10:偶数なので20に10を足す→30

reduce
を使わず上記処理を表現してみるとこんな感じになった。
sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
sum_only_even_number_list = []
acc = 0
for num in sample_list:
if num % 2 == 0:
sum += num
sum_only_even_number_list.append(sum)
else:
sum_only_even_number_list.append(sum)
listにappendする部分も含めてfor文で回しているが、やっていることはかなりシンプルだと分かる。

何も実行しない
→0を足す
と考える
- 奇数がlistから与えられた時→ここまでの累積和に0を加算する(記述省略は可能)
- 偶数がlistから与えられた時→ここまでの累積和にその偶数を加算する
((((((((初期値0+0)+2)+0)+4)+0)+6)+0)+8)+0)+10)→0
(((((((((0+2)+0)+4)+0)+6)+0)+8)+0)+10)→2
((((((((2+0)+4)+0)+6)+0)+8)+0)+10)→2
(((((((2+4)+0)+6)+0)+8)+0)+10)→6
((((((6+0)+6)+0)+8)+0)+10)→6
(((((6+6)+0)+8)+0)+10)→12
((((12+0)+8)+0)+10)→12
(((12+8)+0)+10)→20
((20+0)+10)→20
(20+10)→30

reduceとlambda式を使って書くとこんな形になる
sum_only_even_number_list = reduce(lambda acc, cur: [偶数が与えられた時の処理] if cur % 2 == 0 else [奇数が与えられた時の処理], sample_list, [])

reduceでまとめていく値をlistに収めるには、
[*acc(今までの計算結果を入れているlistのアンパック), 今回追加でlistに入れる要素の計算]
という風にここまでの計算結果のlistに今回対応する要素を追加する形で記述していく必要がある。
今回追加でlistに追加する要素の計算は以下のようになる。
偶数が与えられた時
ここまでの累積和に今回与えられた偶数を足す acc[-1] + cur
奇数が与えられた時
ここまでの累積和の値に0を足す(0を足すところは省略できる) acc[-1]
ここまでを式に書くと以下のようになる。
sum_only_even_number_list = reduce(lambda acc, cur: [*acc, acc[-1] + cur] if cur % 2 == 0 else [*acc, acc[-1]], sample_list, [])

上記の式を実行するとIndexError: list index out of range
というエラーが出る。
これは、「ここまでの累積和」を利用するよう書いていた結果初回の計算実行時にacc[-1]で取り出せる値が存在しないためエラーが発生してしまったということだ。
初回の実行時をどう認識させるかについてだが、まだここまでの累積和をリストに入れていない空リストの状態(三番目の引数に初期値として[]
を設定している)かどうかを判定することで認識させられる。
つまりリストに要素があるかどうかは、len(acc) != 0
で判定できることになる。
今回の処理が初回だった場合に実施する必要がある処理は、以下のようになる。
偶数が与えられた場合
その偶数を要素に入れる cur
奇数が与えられた場合
0を要素に入れる 0
上記の条件と処理を追加した式は以下のようになる。
sum_only_even_number_list = reduce(lambda acc, cur: [*acc, acc[-1] + cur if len(acc) != 0 else cur] if cur % 2 == 0 else [*acc, acc[-1] if len(acc) != 0 else 0], sample_list, [])

最終的なコードは以下のようになった。
from functools import reduce
sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
sum_only_even_number_list = reduce(lambda acc, cur: [*acc, acc[-1]+cur if len(acc)!=0 else cur] if cur % 2 == 0 else [*acc, acc[-1] if len(acc)!=0 else 0], sample_list, [])
print(sample_list)
print(sum_only_even_number_list)
実行結果はこのようになる。
listの先頭が奇数だった時
% python3 reduce.py
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[0, 2, 2, 6, 6, 12, 12, 20, 20, 30]
listの先頭が偶数だった時
% python3 reduce.py
[2, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[2, 4, 4, 8, 8, 14, 14, 22, 22, 32]
reduce
関数は、Python3になり組み込み関数から削除されてfunctoolsモジュールに入れられたので、上記のように呼び出してあげる必要がある。

ワンライナーで書くにはかなりゴツいし、reduceの結果をlistに入れる必要性があるとそれぞれの引数の型意識をしっかり持たないとTypeErrorやIndexErrorと一生顔を突き合わせることになる。
なかなかの良問だった。