高階関数によるデータ処理
商品明細から個数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
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}]def ordered(row):
return row["qty"] > 0selected = list(filter(ordered, rows)); print(selected); print(len(rows), len(selected))[{'name': 'pen', 'price': 120, 'qty': 3}, {'name': 'note', 'price': 200, 'qty': 2}]
3 2amounts = list(map(lambda row: row["price"] * row["qty"], selected)); print(amounts)[360, 400]
def add_amount(total, amount):
result = total + amount
print(total, "+", amount, "=", result)
return resultfrom 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
discounted = list(map(lambda amount: amount * 90 // 100, amounts)); print(discounted, sum(discounted)); print(amounts)[324, 360] 684 [360, 400]
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
print(reduce(lambda left, right: left - right, [10, 3, 2])); print((10 - 3) - 2, 10 - (3 - 2))5 5 9
演習完了