FP:operatorで、ラムダ関数なしに純粋関数にする
このワークシートはMath by Codeの一部です。
1.演算子を関数形式にしよう
<演算子を関数形式にしよう>
純粋関数のメリットはバケツリレーができて副作用がないことだった。
今まではリストと辞書という名詞、コンテナクラス、コレクション群に対して行う
処理(追加、更新、削除)の浄化活動、環境保護活動をやってきた。
しかし、私たちがいつもやる単純なデータに対する処理についてはどうだろう。
四則計算+-*/など、なんの害もない。
大小比較も、害はない。
イン演算子も、ただの構文であり害がない。
たしかに害のない便利な演算子はいろいろある。
しかし、処理、作用としての関数をFPパラダイムで極めたかったらどうだろう。
パイプラインができて欲しくなるね。
四則計算を関数にしてしまおう。
+ (addition),- (subtraction),*(multiplication),/(division)は
は
add(), sub() , mul() ,div()
という名の関数にすればよいね。
では、定義をいちいち書きますか?
いいえ、もう用意されてます。
それが
operator
関数形式の標準演算子
のライブラリだ。
たとえば、
import operator
res7 = operator.add(3, 4) # 対応式 3 + 4
res30 = operator.mul(3,10) # 対応式 3 * 10
大小比較の演算子にも関数がある。
is_greater = operator.gt(5, 3)
これらの関数の名前をかき連ねるだけで、よくある計算式が純粋なパイプラインになる。
#関数形式にすれば、演算子のバケツリレーができる。
res25 = operator .mul(operator. add(2, 3), 5)
print(res25)
2.もっとあるoperatorの関数
もっとあるoperatorの関数
<意外と盲点となる演算子たち>
算術や大小の演算子をわざわざ関数で書く人は少ないかもしれない。
累乗はどうしても、a**bで押し通す人もいるでしょう。operator.pow(a,b)がいいね。
剰余もa%bになれていますよね。geogebraのようにoperator.mod(a,b)がいいね。
ご存知でしょうが、何回も使う関数は、import operatorではなく、
from operator import pow,mod
のように宣言しておくと、operator.
をかかなくても動きます。
行列の積a @ bは、matmul(a,b)でいける。いいですね。
XOR排他的論理和 a^bは、まんまxor(a,b)とかけるのもよいですね。
<コンテナ用の演算子もある>
数値や論理以外にもある。
シーケンス加算a + bは、なつなしい名前concat(a, b)でどうぞ。
コンテナAに対する演算子の関数があります。
コンテナAにbあるかは、ただ英語にしただけのcontains(A, b)です。主語Aが先、目的語がbの順。
コンテナAにbがある回数は、countOf(A, b)です。これは便利そうだね。
コンテナ演算子関数の本命はgetitemです。
これは、あとでまた登場します。
operator.getitem(A, idx)
インデクスがidx の値を返します。A[idx]です。
Aがリストならば、idxは数字ですが、
Aが辞書ならが、idxはキーになります。
コンテナAとしたのは、コンテナになれる物なら何でも使えるからです。
from operator import getitem
soldier = dict(rank='captain', name='dotterbart')
getitem(soldier,'rank')
[OUT]
captain
しかし、setitem(a,b,c), delitem(a,b)は破壊的な関数ですから、
ご注意ください。
3.抜き出し関数
もっと複雑なことを簡単にできる関数があります。
抜き出し、並べ替えに使えるプロ用関数、itemgetterです。
<itemgetterの基本>
operator.itemgetter(item)
operator.itemgetter(*items)
裏でgetitemを使う手の込んだ関数です。
演算対象からその __getitem()__ メソッドを使ってitemを取得する呼び出せる(callable)オブジェクトを返します。次がitemgetterの定義です。
def itemgetter(*items):
if len(items) == 1:
item = items[0]
def g(obj):
return obj[item]
else:
def g(obj):
return tuple(obj[item] for item in items)
return g
引数*itemsがidxが1個なら「g(obj)というobj[idx]を渡す関数」を返します。
だから
from operator import itemgetter
r=['a','b','c','d']
f = itemgetter(2)
f(r)
[OUT]
c
つまり、インデクスを先にして、オブジェクトをあとで指定できます。
itemgetter(idx)(obj)と渡せます。
だから、
itemgetter(2)('abcd')
でも同じ出力になります。
引数*itemsのidxが複数なら、
「*itemsの中のitemそれぞれに対応するobj[item]をずらりとならべた関数群タプル」
を返します。
たとえば、
>>>itemgetter(1, 3, 5)('ABCDEFG')
('B', 'D', 'F')
>>>itemgetter(slice(2, None))('ABCDEFG')
'CDEFG'
<itemgetterの応用>
itemはobjの __getitem()__メソッドが受け付けられるならどんな型もOKです。
2番目に投げるobjがリスト、タプル、文字列なら、スライスでもよいです。
>>>itemgetter(slice(2, None))('ABCDEFG')
'CDEFG'
辞書ならばキーを渡せるのです。getitem(A,idx)でidxに辞書Aのキーを渡せたことを思い出そう。
>>>soldier = dict(rank='captain', name='dotterbart')
>>>itemgetter('rank')(soldier)
'captain'
from operator import itemgetter
inventory = [('apple', 3, 300), ('banana', 2,100), ('pear', 5, 200 ), ('orange', 1,300)]
#果物の個数を数え値段をつけた辞書を作った果物山。
getcount = itemgetter(1) #アイテムの個数を読む。
getprice = itemgetter(2) #アイテムの値段を読む。
list(map(getcount, inventory)) #棚おろしの果物山のアイテムの個数を読み取り、リスト化
[OUT]
[3, 2, 5, 1]
sorted(inventory, key=getcount) #果物山のアイテム個数で並べ替える。非破壊的。
[OUT]
[('orange', 1, 300), ('banana', 2, 100), ('apple', 3, 300), ('pear', 5, 200)]
sorted(sorted(inventory, key=getcount) ,key = getprice)
#果物山を個数でソートしたあと、値段でソート。安定ソートだから、300のorange,appleは個数順のまま
[OUT]
[('banana', 2, 100), ('pear', 5, 200), ('orange', 1, 300), ('apple', 3, 300)]
<振り返り>
もし
operator を使わずにこれらを書こうとすると、
sorted(inventory, key=lambda x: x[1])
というようにlambda をかく手間がかかり見づらいくなりますね。4.呼び出し関数
<属性を呼べる関数>
itemgetterのインスタンス検索版です。itemがattrに対応しています。
operator.attrgetter(attr)、
operator.attrgetter(*attrs)
対象インスタンスobjのattr を取得する関数を返します。
使い方は、
f = attrgetter('name') とした後で、f(b) を呼び出すと b.name を返します。
f = attrgetter('name', 'price') とした後で、f(b) を呼び出すと (b.name, b.price)
を返します。
たとえば、
from operator import attrgetter
class PC:
def __init__(self, name, price):
self.name = name
self.price = price
products = [
PC('Mac', 200000),
PC('Win', 170000),
PC('Google', 100000)
]
# price属性でソート
sorted_by_price = sorted(products, key=attrgetter('price'))
for p in sorted_by_price:
print(f"{p.name}: {p.price}")
[OUT]
Google: 100000
Win: 170000
Mac: 200000
<関数を呼べる関数>
methodcaller(name, /, *args, **kwargs)
引数に関数nameを呼び出せるオブジェクトobjを渡すとobj.name()を返します。
/の区切りのあとは、その関数がつかう引数で、kwargsはkwargs=とわたすキーワードです。
f = methodcaller('name') とした後で、f(b) を呼び出すと b.name() を返します。
f = methodcaller('name', 'foo', bar=1) とした後で、
f(b) を呼び出すと b.name('foo', bar=1) を返します。
def methodcaller(name, /, *args, **kwargs):
def caller(obj):
return getattr(obj, name)(*args, **kwargs)
return caller
from operator import methodcaller
# 複数の引数を渡す例
class Calc:
def add(self, a, b):
return a + b
def sub(self, a, b):
return a - b
def mul(self, a, b):
return a * b
def div(self, a, b):
return a / b
def pow(self, a, b):
return a**b
ca = Calc()
print(methodcaller('add', 5, 3)(ca)) # ➔ 8
print(methodcaller('pow', 5, 3)(ca)) # ➔ 125
<振り返り>
このmethodcallerの例は簡単すぎる計算なのでぱっとしませんが、
部品関数をクラス化するというのが面白い。
引数を関数によって自由に変えらる柔軟性が光っている。
itemgetter,attrgettr,methodcaller
ともに、細部を指定しておき、
あとでコンテナデータ(辞書、リスト、データクラス)を
渡すという順番がこれらの関数の使い方のポイントになります。
どの項目、どの属性、どのキーに着目するかを決めてからデータを走査、操作するのです。
やっていることはlambda関数を使うなどの処理ですが、
わかりやすく、しかも安全なのがいいね。
また、定義が公開されているので、動きが予測できるのもいいね。
課題:methodcaller風の動きをgeogebraで作ろう。
add=a+b
sub=a-b
mul=a b
div=a/b
pow=a^b
n=slider(1,5,1)
name={"add","sub","mul","div","pow"}
calc={add,sub,mul,div,pow}
c=calc(n)
text1=tabletext(name,calc,"h")
text2="methodcaller(" + name(n) + "," + a +"," + b + ")(calc)=="+c+""
これで、スライダa,b,nを動かしてみましょう。キーからの呼び出しではありせんが、
nをキーの指定だと思えばItemファースト、データラストが実感できるかもしれませんね。