一覧PG-15-1 · 関数を組み合わせる

高階関数によるデータ処理

商品明細から個数0の行を除き、各行の金額と合計を求めます。処理に渡す関数を変えて割引を加え、最後に空の入力や計算順が集計へ与える影響を確かめます。

説明

関数は呼び出すだけでなく、値として別の関数へ渡せます。mapは各要素の変換、filterは条件に合う要素の抽出、reduceは累積値の更新に、渡された関数を使います。

商品明細から個数0の行を除き、各行の金額と合計を求めます。処理に渡す関数を変えて割引を加え、最後に空の入力や計算順が集計へ与える影響を確かめます。

1同じ明細を処理の出発点にする

商品名、単価、個数を持つ三つの明細を用意します。

rows =明細一覧を作る [{"name": "pen", "price": 120, "qty": 3}, {"name": "book", "price": 500, "qty": 0}, {"name": "note", "price": 200, "qty": 2}]; print(rows)処理前の値

penは360、bookは0、noteは400です。今回は個数が正の明細だけを請求対象とします。以後の処理でもこの元の一覧を残して比較します。

2抽出条件を関数にする

個数が正かどうかを判定する関数を定義します。

def ordered(row):一件を受け取る判定関数 return row["qty"] > 0請求対象の条件

関数は明細を一件受け取り、対象ならTrueを返します。filterに渡すと、同じ判定が入力の各明細へ適用されます。

3条件に合う明細だけを取得

filterにはorderedという関数自体を渡します。ordered()と書いて先に呼ぶ形ではありません。

selected = list反復結果を一覧にする(filter(ordered, rows)各明細へ条件関数を適用); print(selected); print(len(rows), len(selected)元の件数と抽出件数)

penとnoteの2件が残り、元のrowsは3件です。filterの返す反復子をlistで消費して一覧にしています。抽出結果に含まれる辞書は元と同じ参照なので、この操作は各明細の深いコピーではありません。

4各明細を金額へ変換

各行の単価と個数を掛ける短い関数をlambdaで渡します。

amounts = list(map一件ずつ変換(lambda row: row["price"] * row["qty"]明細から金額を求める関数, selected変換する明細)); print(amounts)

金額は360と400です。lambdaは式一つからなる関数を作ります。mapは各入力に対して一つずつ戻り値を得るため、ここでは明細2件が金額2件になります。

5累積値の変化を表示

reduceへ渡す関数は、現在の累積値と次の値を受け取ります。途中の計算も表示する関数を作ります。

def add_amount(total, amount):累積値と次の金額 result = total + amount次の累積値 print(total, "+", amount, "=", result) return result次回へ渡す値

戻り値が次の呼出しの累積値になります。この関数のprintは、今回の集計順を確認するための表示です。

6初期値0から集計

reduceの初期値に0を渡して合計を求めます。

from functools import reduce累積処理を読み込む; total = reduce(add_amount, amounts, 0)0から順に累積; print("total", total, "sum", sum(amounts)合計専用の操作と照合)

0+360=360、360+400=760の順です。合計だけが必要ならsum(amounts)で同じ760を求められます。reduceは、加算以外も含めて累積の更新方法を関数として渡せる操作です。

7変換する関数を差し替える

各明細の金額を10%引きにします。この演習では金額を整数で扱い、小数部は各明細ごとに切り捨てます。

discounted = list(map(lambda amount: amount * 90 // 10010%引き・各行で切捨て, amounts元の金額一覧)); print(discounted, sum(discounted)割引後の合計); print(amounts)元の一覧を確認

割引後は324と360で合計684です。元の金額一覧は360と400のままです。mapへ渡す関数を変更すると、同じ入力一覧へ別の変換を適用できます。

8空の一覧と初期値

空の入力へreduceを適用し、初期値を指定した場合と省略した場合を確認します。

try: print(reduce(add_amount, [], 0)初期値のある空入力) reduce(add_amount, [])初期値のない空入力 except TypeError開始値がない失敗 as error: print(type(error).__name__, str(error))

初期値0を渡せば結果は0で、add_amountは一度も呼ばれません。初期値を省略した空の入力では、累積を始める値がないためTypeErrorになります。

9順序で結果が変わる累積

減算をreduceに渡し、括弧の位置を変えた式と比較します。

print(reduce(lambda left, right: left - right, [10, 3, 2])左から減算を累積); print((10 - 3) - 2, 10 - (3 - 2)括弧の位置を比較)

reduceは左から累積するので(10-3)-2=5です。10-(3-2)=9とは異なります。加算で同じ結果になるからといって、任意の関数で順序やまとめ方を変えてよいわけではありません。

まとめ

関数自体を引数へ渡すと、同じ一覧に異なる抽出条件や変換を適用できます。

mapは変換、filterは抽出、reduceは累積値の更新です。単純な合計にはsumも使えます。

空入力の初期値や、順序を変えても同じ結果になるかを確認して集計を設計します。

操作履歴と解説を振り返れます。

Python 3

1
rows = [{"name": "pen", "price": 120, "qty": 3}, {"name": "book", "price": 500, "qty": 0}, {"name": "note", "price": 200, "qty": 2}]; print(rows)
[{'name': 'pen', 'price': 120, 'qty': 3}, {'name': 'book', 'price': 500, 'qty': 0}, {'name': 'note', 'price': 200, 'qty': 2}]
2
def ordered(row): return row["qty"] > 0
3
selected = list(filter(ordered, rows)); print(selected); print(len(rows), len(selected))
[{'name': 'pen', 'price': 120, 'qty': 3}, {'name': 'note', 'price': 200, 'qty': 2}]
3 2
4
amounts = list(map(lambda row: row["price"] * row["qty"], selected)); print(amounts)
[360, 400]
5
def add_amount(total, amount): result = total + amount print(total, "+", amount, "=", result) return result
6
from functools import reduce; total = reduce(add_amount, amounts, 0); print("total", total, "sum", sum(amounts))
0 + 360 = 360
360 + 400 = 760
total 760 sum 760
7
discounted = list(map(lambda amount: amount * 90 // 100, amounts)); print(discounted, sum(discounted)); print(amounts)
[324, 360] 684
[360, 400]
8
try: print(reduce(add_amount, [], 0)) reduce(add_amount, []) except TypeError as error: print(type(error).__name__, str(error))
0
TypeError reduce() of empty iterable with no initial value
9
print(reduce(lambda left, right: left - right, [10, 3, 2])); print((10 - 3) - 2, 10 - (3 - 2))
5
5 9

演習完了