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