純粋関数・関数合成・部分適用
金額計算を例に、外部変数への依存を引数へ移し、割引と送料を組み合わせます。部分適用で設定を固定した関数を作り、最後は元の明細を残したまま新しい明細を返す更新を行います。
説明
同じ入力から同じ結果を返し、入力や外部の状態を変更しない関数は、結果を再現しやすく、組合せの途中も確認しやすくなります。入力を変更する処理や、隠れた外部変数に依存する処理では、同じ呼出しでも結果が変わる場合があります。
金額計算を例に、外部変数への依存を引数へ移し、割引と送料を組み合わせます。部分適用で設定を固定した関数を作り、最後は元の明細を残したまま新しい明細を返す更新を行います。
1引数以外に依存する計算
割引率を外部変数から読む関数を定義します。
def hidden_discount(amount):金額だけを引数にする
return amount * rate // 100外部のrateも参照amountが同じでも、rateが変われば結果が変わります。呼出しの引数だけでは、計算条件を確定できません。
2同じ引数で結果が変わる
同じ金額1000で、外部の率だけを変えます。
rate = 90; print(hidden_discount(1000)同じ金額で呼ぶ); rate = 80外部の率を変更; print(hidden_discount(1000)同じ引数で再実行)結果は900から800へ変わります。関数が入力を書き換えなくても、外部状態への依存があれば、引数だけでは結果を再現できません。
3必要な条件を全て引数にする
適用率を明示的な引数にし、整数計算の規則も関数へまとめます。
def discount(amount, percent):必要な二つの条件を受け取る
return amount * percent // 100整数で計算して切り捨てるこの関数は二つの引数だけから結果を計算し、外部状態を変更しません。同じamountとpercentを渡せば同じ値になります。ここでpercentは割引後に残す割合で、90は10%引きです。
4外部変数を変えても同じ結果
外部のrateを変更しても、明示したpercent=90の計算が変わらないことを確認します。
first = discount(1000, 90)金額と適用率を明示; rate = 50外部の値を変更; second = discount(1000, 90); print(first, second, first == second同じ結果か確認)どちらも900です。この呼出しの計算条件は引数に揃っているため、外部のrateを変更しても影響しません。
5一部の引数を固定した関数
partialでpercentを90に固定し、金額だけを受け取る関数を作ります。
from functools import partial部分適用を読み込む; ten_percent_off = partial(discount, percent=90)適用率を固定; print(ten_percent_off(1000), ten_percent_off(500)金額だけを渡す)元のdiscountを書き換えずに、10%引きの計算を繰り返し使えます。部分適用は、引数の一部を先に指定した呼出しを作る方法です。
6送料を加える関数
金額に送料を加える処理を独立した関数にします。
def add_shipping(amount, fee):金額と送料を受け取る
return amount + fee送料込みの金額同じ金額でも、割引してから送料を加えるか、送料込みで割り引くかで最終金額は変わります。次に順序を比較します。
7関数を適用する順序
商品1000、送料200で二通りの順序を実行します。
print(add_shipping(ten_percent_off(1000), 200)商品だけ割引後に送料を追加); print(ten_percent_off(add_shipping(1000, 200))送料を含めて割引)商品を割り引いてから送料を加えると1100、送料込みで割り引くと1080です。内側の関数の戻り値が外側の引数になります。関数を合成するときには、どの金額へ割引を適用する仕様かを先に決めます。
8元の明細を残す更新
明細は商品名と個数のタプルを並べたタプルで保持します。一つの商品の個数だけを変えた新しいタプルを返す関数を定義します。
def set_quantity(items, name, quantity):元の明細と変更内容
return tuple((item, quantity if item == name else old対象だけ新しい個数) for item, old in items元の各明細を処理)既存の明細をその場で変更せず、新しい明細を返します。この関数は同名の商品があれば全て置き換え、該当商品がなければ内容が同じタプルを返す仕様です。
9更新前後を同時に保持
penの個数を3から5へ変え、同じ入力でもう一度更新します。
original = (("pen", 3), ("book", 1))変更前の明細; updated = set_quantity(original, "pen", 5)新しい明細を作る; repeated = set_quantity(original, "pen", 5)同じ入力で再計算; print(original, updated, repeated == updated内容の再現を確認)originalは3のまま、updatedは5です。同じ入力からのrepeatedはupdatedと等しい内容になります。更新前の値も残るので、比較や取消しの設計へ利用できます。
まとめ
同じ入力から同じ結果を得るには、外部状態への依存も確認します。必要な条件を引数へ揃えると再現しやすくなります。
部分適用で条件の一部を固定できます。関数の合成順は処理の仕様に合わせて決めます。
元の入力を変更せず新しい値を返すと、更新前後を同時に保持して比較できます。
操作履歴と解説を振り返れます。
Python 3
def hidden_discount(amount):
return amount * rate // 100rate = 90; print(hidden_discount(1000)); rate = 80; print(hidden_discount(1000))900 800
def discount(amount, percent):
return amount * percent // 100first = discount(1000, 90); rate = 50; second = discount(1000, 90); print(first, second, first == second)900 900 True
from functools import partial; ten_percent_off = partial(discount, percent=90); print(ten_percent_off(1000), ten_percent_off(500))900 450
def add_shipping(amount, fee):
return amount + feeprint(add_shipping(ten_percent_off(1000), 200)); print(ten_percent_off(add_shipping(1000, 200)))1100 1080
def set_quantity(items, name, quantity):
return tuple((item, quantity if item == name else old) for item, old in items)original = (("pen", 3), ("book", 1)); updated = set_quantity(original, "pen", 5); repeated = set_quantity(original, "pen", 5); print(original, updated, repeated == updated)(('pen', 3), ('book', 1)) (('pen', 5), ('book', 1)) True演習完了