Python Toolzの使い方 Functoolz編

今回はToolzの中からFunctoolzを紹介します。

サンプルコードは全て”from toolz.functoolz import *”の様なインポート文を省略しています。

identity(x)

identity引数の値をそのまま返します。

一見すると使う意味が無いような関数ですが、関数型プログラミングにおいては重要な関数の一つです。モナドを扱うときに活躍します。

result = identity(3)
print(result)
# 3

thread_first(val, * forms )

一連の関数/フォームを実行した結果を返します。

以下の例は1をinc関数に渡し2が返され、次にその戻り値をdouble関数に渡しています。

def double(x): return 2*x
def inc(x): return x + 1
result = thread_first(1, inc, double)
print(result)
# 4

一般的にthread_first(x, f, (g, y, z))は、数式のg(f(x), y, z)として表現できます。

似た様なthread_last()という関数もあります。これは関数の最後の引数にvalを与える点がthread_first()と異なります。

一般的にthread_last(x, f, (g, y, z))は、数式のg(y, z, f(x))として表現できます。

compose(*funcs)

引数の関数を右から順番に適用する関数を返します。

compose(f, g, h)(x, y)は関数は右から左に適用されるため、 f(g(h(x, y)))と同じになります。

引数が指定されていない場合、恒等関数(f(x)= x)が返されます。

inc = lambda i: i + 1
result = compose(str, inc)(1)
print(result)
# 2

pipe関数に似ていますが、composeは合成した関数を使いまわしたい時に便利です。

pipe(data, *funcs )

複数の関数から成る一連の処理に値を送ります。

pipe(data, f, g, h) = h(g(f(data))) です。

また、この機能はlinuxのpipeコマンドと同様です。右から順に処理されるため、compose関数より読みやすいです。

double = lambda i: 2 * i
result = pipe(2, double, str)
print(result)
# 4

応用例

次の様にmap, filter, getと組み合わせて特定の条件を満たすデータを選択、結合することができます。

from toolz.curried import pipe, map, filter, get

accounts = [(1, 'Alice', 100, 'F'),
             (2, 'Bob', 200, 'M'),
             (3, 'Charlie', 150, 'M'),
             (4, 'Dennis', 50, 'M'),
             (5, 'Edith', 300, 'F')]

result = pipe(accounts, filter(lambda acc: acc[2] > 150), map(get([1, 2])), list)
print(result)
# [('Bob', 200),('Edith', 300)]

このコードはリスト内包表記を使って次の様に書けます。

[(name, balance) for (id, name, balance, gender) in accounts if balance > 150]

complement(func )

通常Trueを生成する入力に対してFalseを生成する関数を返します。逆の場合も同様です。

以下の例では、偶数であればTrueを返すiseven関数をcomplement関数を使って、偶数であればFalseを返す関数(=奇数であればTrueを返す関数)を得ています。

def iseven(n): return n % 2 == 0
isodd = complement(iseven)

result = iseven(2)
print(result)
# True

result2 = isodd(2)
print(result2)
# False

do(func,)

func(x)を実行し、xを戻します。

func関数の戻り値が返されないため、func関数には副作用のある関数を指定すべきです。

例えば、ロギング機能はlist.appendやfile.writeのようなストレージ機能とdo関数で構成することで作成できます。

以下の例ではinc(1)は do(log.append, 1)を実行しています。

log.append(1)では戻り値はありませんが、do(log.append, 1)とすることで、log.append(1)を実行し1を返します。

do関数を使うことで、戻り値のない関数(副作用のある関数)を戻り値のある関数として扱うことができます。メソッドチェーンを作りたいときに役立ちます。

from toolz import compose
from toolz.curried import do

log = []
inc = lambda x: x + 1
inc = compose(inc, do(log.append))

inc(1)
inc(11)
print(log)
# [1, 11]

curry(* args, ** kwargs )

呼び出し可能関数をカリー化します。

不完全な引数で関数を呼び出すと、引数の部分適用を行います。

カリー化とは?でカリー化について詳しく解説しています。

def mul(x, y):
    return x * y

mul = curry(mul)
double = mul(2)
result = double(10)
print(result)
# 20

キーワード引数もサポートしています。

@curry
def f(x, y, a=10):
    return a * (x + y)

flip

引数を反転して関数呼び出しを呼び出します。

flip関数によって出力される関数はカリー化されています。

def div(a, b):
    return a // b

result = flip(div, 2, 6)
print(result)
# 3

div_by_two = flip(div, 2)
result = div_by_two(4)
print(result)
# 2

これは、組み込み関数、および位置引数のみを受け入れるC拡張で定義された関数に特に役立ちます。例:isinstance、issubclass。

以下の例では、まずflip関数でisinstance(data, type)関数の引数を反転させtype = intとしています。

次にこの関数をfilter関数の第1引数とし、第2引数のdataからint型のデータをフィルタするイテレータにします。そのイテレータをlist関数を使ってリスト化しています。

data = [1, 'a', 'b', 2, 1.5, object(), 3]
only_ints = list(filter(flip(isinstance, int), data))
print(only_ints)
# [1, 2, 3]

excepts(exc, func, handler = <function return_none>)

例外をキャッチしてハンドラーにディスパッチ(処理を振り分ける)する関数のラッパーです。

excepts関数はtry / exceptブロックの機能に似ています。

第一引数のexcは第二引数のfuncで発生し得る例外オブジェクトを指定します。タプルやリストを使って複数指定することも可能です。

第二引数のfuncは例外が発生し得る関数です。

第三引数は第二引数のfuncを実行し、第一引数の例外が発生した場合に処理される関数です。何も指定しない場合はNoneが返されます。

この関数を使うことで実行したい処理と、例外発生時の処理を分離しやすくなります。

副作用を持つ関数でもexcepts関数を使うことで副作用を持たない関数のように振舞わせることが可能になりメソッドチェーンで扱えるようになります。

excepting = excepts(
    ValueError,
    lambda a: [1, 2].index(a),
    lambda _: -1,
)
result1 = excepting(1)
print(result1)
# 0

result2 = excepting(3)
print(result2)
# -1

exceptingは、最初に第二引数のfuncが実行されます。

excepting(1)では最初に[1, 2].index(1)が実行され0が返されます。

excepting(3)では最初に[1, 2].index(3)が実行されます。しかしValueErrorが発生します。

次に第一引数にValueErrorがあるかチェックします。ValueErrorがあるため、次に第三引数のhandlerが実行され-1が返されます。

複数の例外とデフォルトのexcept節を使った例を4つ紹介します。

excepting = excepts((IndexError, KeyError), lambda a:a[0])
excepting([])  # None (IndexErrorのため)
excepting([1])  # 1
excepting({})  # None (KeyErrorのため)
excepting({0:1}) # 1
スポンサーリンク
スポンサーリンク