FP:関数ツールで、関数を構造から見直そう
このワークシートはMath by Codeの一部です。
0。functoolsでPythonの幅を広げよう
functoolsの主な道具は3つあります。
reduce():
これは、sumなどの縮約を一般化する高階関数です。
Haskellのように畳み込みする関数を広げることができるようになります。
partial():
多変数関数があるとき、1変数で部分的な関数を作り、残りのパラメータを変化させるような
段階的な設計ができます。
@lru_cache:
処理中に、以前の結果をメモ化するデコレータです。
このデコレータで再帰処理などのパフォーマンスを大幅に向上させることができます。
1.reduceで反復的なアルゴリズムを関数にする
reduceで「反復的にぱたんぱたんと畳んでいく」アルゴリズムを関数にする
<当たり前すぎるreduceアルゴリズム>
カンタンすぎて見失いがちですが、
リストの値の合計を求めるsum()
リストの要素数を求めるlen()
リストの最大値,最小値を求めるmax(),min()は
アルゴリズムは共通です。
それは、リストの左から始めて次つぎと順に同じことをして、
ぱたん、ぱたんと畳んでいき、
最後に結果の数値を出す。
ということです。
つまり、ドミノ倒しです。
左端から右端まで順にぜんぶ倒れます。
初期値をx=0としたとき、
xに次の要素yをたした結果をxとして畳み込むのがsumです。
結果を畳み込むことreduceとかき、xに次の要素yをたした結果をxとするを「lamdba x, y : x+y」
とかき、対象リストと初期値を引数にかけばよいですね。
from functools import reduce
L=[1,2,3,4,5,6,7,8,9,10]
sum=reduce(lambda x, y: x+y, L,0)
sum
[OUT]
55
そうすると、
len=reduce(lambda x,y : x+1,L,0) #len=10
max=reduce(lambda x,y: x if x>y else y,L) #初期値は与えないので、デフォルトはリストの先頭
<いろいろ作れるラムダ式>
たとえば、リストLの各要素を2乗した合計を求める関数が作れます。
from functools import reduce
L=[1,2,3,4,5,6,7,8,9,10]
def squareSum(L):
return reduce(lambda x,y:x + y**2,L,0)
squareSum(L)
ラムダ式は、やっていることがそのままかけるので、
プログラムで何をやっているのが明白になります。
コメントなしでもわかります。
2乗和といえば、標準偏差の計算を連想しますね。
では、
標準偏差を求める関数を、標準関数に頼らずに、
できるだけreduceとラムダ式でかいてみよう。
from functools import reduce
L=[1,2,3,4,5,6,7,8,9,10]
def STD(L):
sum=reduce(lambda x, y: x+y, L,0)
count=reduce(lambda x,y : x+1,L,0) #count=10
av = sum/len # 平均
dev = [x-av for x in L] # 偏差deviation
var = reduce(lambda x,y:x + y**2,dev,0)/count # 分散varianceは偏差の2乗和の平均
return (var)**0.5 #STD標準偏差 standard deviation
STD(L)
[OUT]
2.8722813232690143
さて、今まではxのあとが[+]が多かった。
これを[*]にすると、累加を累乗に変えられます。
そう、階乗計算ですね。
def factorial(n):
return reduce(lambda x, y: x*y, range(1, n+1))
print(factorial(5)) # 120
もしも、純粋さを徹底するならば、かけ算も関数にした高階関数HOFすることもできるね。
from functools import reduce
from operator import mul
def factorialHOF(n):
return reduce(mul, range(1, n+1), 1)
それと、1から順にたすのはsumで総和ですね。
1から順にかけるのはproductです。総和でなく総積? 総積を高階関数作ってみよう。 オペレーターモジュールで積はmulだったから、
from functools import reduce
from operator import *
L=[2,3,4,5]
product = reduce(mul,L)
product #120
さて、このmul、かけ算を累乗のpowにしてみよう。
power_chain = reduce(pow,L)
power_chain #1152921504606846976
これはすごい、((2^3)^4)^5)が前回やった演算子の関数化という発想とくっつけると
すごいことがカンタンにかける。
これぞ畳み込みというイメージピッタリの迫力だね。
<振り返り> そうはいっても、必要がなければ、ラムダ式は見慣れない人には煩雑にみえる。 だから、同じことをするには次の3つの式のどれか、状況で使い分けよう。 Lがリストのように、イテラブル(左から順に右へと進めるもの)なら、 reduce(lambda a, b: a+b, L, 0)
reduce(operator.add, L, 0)
sum(L) 合計するだけなら、一番最後でぜんぜんかまわないね。
2.多変数関数を1変数関数の合成関数とみるパーシャル
<多変数関数を1変数関数の合成関数とみるパーシャル>
たとえば、不動産物件のGUIを作るにはどうしますか。
売るか買うか貸すか借りるかの種別、
希望の価格帯、
エリアなど複数の条件から選択して、
それらにあうものがリスト表示されるというのが考えられますね。
タブ形式にして売ると買う貸す借りるを別にしておき、その中でさらに探す方が使いやすいですね。
全国展開している会社ならば、日本地図を県や地域にわけて、それを選択してから、先に進むと
条件を絞りやすいですね。
不動産に限らず、宿泊とかのサイト・アプリなど、
条件がたくさんからむものは
何かしら、先に条件を限定する仕組みがあります。
これを関数としてみると、たくさんの条件、つまり、
多変数から決めたり、絞り込んだりするということです。
そのとき、いっぺんに変数に具体化して数値などを入れ込むのではなく、
段階的に処理できる方が便利ですね。
これは、日常生活にかぎらず、データ処理ではとても高いニーズがあります。
多変数関数の段階処理に役立つpartialを見ていこう。
<多変数関数に変数を減らした新関数に仕立てる>
関数funcをpartialでくるむと新しい関数newfuncオブジェクトを返します。
この新関数newfuncは呼び出されると位置引数 args とキーワード引数keywords 付きで呼び出された func のように振る舞います。呼び出しに際してさらなる引数が渡された場合、それらは位置引数 argsのあとに 付け加えられ、追加のキーワード引数が渡された場合には、それらで keywords を拡張または上書きされます。
おおよそ次のコードと等価です:
def partial(func, /, *args, **keywords):
def newfunc(*more_args, **more_keywords):
return func(*args, *more_args, **(keywords | more_keywords))
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc
もっとざっくりいうと、引数を1つ固定した関数を作っておいて、残った変数だけ変えて使えるようにするということです。
<2変数が対等なとき>
次のように、気楽に使えます。
#2変数が対等な場合
from functools import partial
def multiply(x, y):
return x * y
#multiply関数の変数2を適用して固定した部分関数dblを作る。
dbl = partial(multiply, 2)
print(dbl(5)) #10
print(dbl(6)) #12
print(dbl(7)) #14
<2変数が対等でないとき>
from functools import partial
def power(base, exponent):
return base ** exponent
#power関数の位置引数baseに2を適用して固定した部分関数pow2を作る。
#位置引数ですから、キーワードは不要ですね。
pow2 = partial(power, 2)
print(pow2(5)) #32
print(pow2(6)) #64
print(pow2(7)) #128
つまり、partialによって、次のラムダ式をかいたことになります。
pow2 = lambda y: power(2, y)
from functools import partial
def power(base, exponent):
return base ** exponent
#power関数のキーワード引数exponentに2を適用して固定した部分関数squareを作る。
square = partial(power, exponent=2)
print(square(5)) #25
print(square(6)) #36
print(square(7)) #49
つまり、partialによって、次のラムダ式をかいたことになります。
square = lambda x: power(x, 2)
<パーシャルを便利に使う>
ここまでなら、2変数ならラムダ式の方がわかりやすい。パーシャルはいらないかも。
と思うでしょう。
変数が多くなると状況は変わってきます。
消費税が品目によって変わる国があります。日本もそうです。
しかし、基本の式は同じですよね。
#定価に(1-割引率)をかけて、売価を出す。
#売価に(1+消費税)をかけて税込みの代金を計算します。
from functools import partial
def calculate_price(discount, tax_rate, price):
discounted_price = price * (1 - discount)
return discounted_price * (1 + tax_rate)
#あるスーパーで今日は全品10%割引デーだとしよう。
#discount=0.10に固定した今日専用の関数を作りましょう。
#消費税率 tax_rateが10%が基本ですが、食料品などは8%で代金計算します。
tax_inc_10 = partial(calculate_price, 0.10,0.1)
tax_inc_08 = partial(calculate_price, 0.10,0.08)
print(int(tax_inc_10(1000))) #990
print(int(tax_inc_08(1000))) #972
引数を左から順に固定する場合は位置引数の意味になるので、引数名は不要です。
それがわかると、使いやすいですね。
ということは、「partialをかませたい関数を作るときの作戦」が浮かび上がってきましたね。
引数の順序を「変わりにくい順」にする関数を定義するとき、
「設定(変わりにくい値)」を左側に集め、
「データ(一番最後に流れてくる主語)」を一番右に配置します。
DataLastの原則がまた登場!
3.デコレータを使ってみよう。
デコレータを使ってみよう。
<@lru_cacheでメモ化>
再帰関数は数学の漸化式をそのまま定義にして使えるので、
とても作りやすいです。
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(300))
このままでは、PCはたぶん固まります。
そのままだと、スタックに積んでは降ろして、倍々の無駄な計算をします。
同じ計算式を山のように計算してしまうからです。
たとえば、fibonacci(100)を出すためには、
fibonacci(90)とfibonacci(89)の計算をしますが、
fibonacci(90)の計算の山の中にfibonacci(89)の計算の山が入ってます。
およそ、nを1増やすたびに、計算量が2倍近くになることはイメージできますね。
計算の山というのがピンとこないなら、小さい順に考えてみよう。
たとえば、1+1はnが3以上ならどのfibonacci計算式の最後にたどり着きます。
そして、そこから計算式の実行が積み重なるます。
fibonacci(3)=fibonacci(1)+fibonacci(2)=1+1=2
毎回、この計算から計算を積み上げているわけです。
これがわかると、再帰計算が固まってみえる理由がわかりますね。
その救世主が@lru_cacheです。
一度fibonacci(3)を計算したら、二度とfibonacci(3)を汚しません。
再計算しません。だから、@lru_cacheを先頭につけた再帰関数は高速なのです。
次のように2行2追加しただけで一瞬で計算が終わります。
これをメモ化といいますね。
キャッシュメモリを使い、動的計画法という方式を使うことで、
このデコレータでくるむ、ラップされた関数を高速化しているのです。
競技プログラミングのようにスピードを競うプログラミングでは
動的計画法やメモ化は必須の手段のようですね。
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(100))
print(fibonacci(200))
print(fibonacci(300))
[OUT]
354224848179261915075
280571172992510140037611932413038677189525
222232244629420445529739893461909967206666939096499764990979600
ちなみに、Python 3.9からは、
from functools import cache
@cache
とカンタンな書き方になりました。
課題:geogebraでパーシャル的な発想はできますか。
geogebraは関数の部分適用はできませんが、関数合成はできます。バケツリレーです。
一般関数はツールやjavascriptにたよらないと作れませんが、座標関数は作れます。
これを前提にして、
消費税率1%、8%、10%で1000円の定価のものを1割引きにした代金を求める
関数合成をやってみよう。
タイトルは「消費税の計算を分解合成しよう」
n=slider(1,3,1)
discountRate=0.1
taxRate={0.01, 0.08, 0.1}
f = (1 - discountRate) x
g = (1+taxRate(n)) x
h= g(f(x))
price=slider(1000,10000,1000)
pay=h(price)
text1 = Text("" + price + "円の1割引きの税込み支払代金は" + pay + "円", (1, 1))