Closed9

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に処理させても良い。

https://github.com/cm-nakamura-shogo/python-training/tree/master

無名関数での実装は難しいと思われる、らしいので無名関数で実装してみることにした。

ちゃこちゃこ

動きとしては全ての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文で回しているが、やっていることはかなりシンプルだと分かる。

ちゃこちゃこ

https://zenn.dev/link/comments/b080e3c6f6cb01
この考え方だとreduceで動きを考える時にちょっと違和感があるので、
何も実行しない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, [])
ちゃこちゃこ

最終的なコードは以下のようになった。

reduce.py
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モジュールに入れられたので、上記のように呼び出してあげる必要がある。
https://docs.python.org/3.0/whatsnew/3.0.html#builtins

ちゃこちゃこ

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

このスクラップは2024/05/30にクローズされました