Google ClassroomGoogle Classroom
GeoGebraGeoGebra Classroom

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))

    消費税の計算を分解合成しよう