Background
Scalaのコレクション操作かっこいいですよね。余計な中間変数とか作らずにすっきり書けます。
val result = (0 to 10000)
.filter(_ % 3 == 0)
.map(_ + 1)
.groupBy(_ % 10)
.map { it =>
val k = it._1
val v = it._2.sum
(k, v)
}.toList
このコードは0から10000までの数字を3の倍数だけ残して1足して10で割ったあまりでグループ分けした時のそれぞれの数値の和です。この計算に特に意味は無いですが,データ処理フローをこのように思考の順番と全く同じで非常にわかりやすく(かっこ良く)書ける例です。
これをPythonでやろうとすると...
import itertools
result = range(0, 10001)
result = filter(lambda x: x % 3 == 0, result)
result = map(lambda x: x + 1, result)
result = map(lambda x: (x % 10, x), result)
result = sorted(result)
result = itertools.groupby(result, lambda x: x[0])
result = map(lambda x: (x[0], sum(map(lambda _: _[1], x[1]))), result)
result = list(result)
見難くて目も当てられません。ちなみに中間変数を使用せずに一発で書くと
result = list(
map(lambda x: (x[0], sum(map(lambda _: _[1], x[1]))),
itertools.groupby(
sorted(
map(lambda x: (x % 10, x),
map(lambda x: x + 1,
filter(lambda x: x % 3 == 0,
range(0, 100001)
)
)
), lambda x: x[0]
)
)
)
)
可読性0のコードの出来上がり。これがすんなり読めるようになると何かに目覚めるかも知れません。関数の宿命というか,どうしてもf -> g -> hの順で処理しようと思うとh(g(f(x)))のように逆の順番に書かないといけないのが原因です。
実はこれを解決するライブラリがあるんです。そうtoolz
とscalafunctional
とfn.py
ならね。この記事ではScalaで書けなど意見はNGワードです。
Toolz, CyToolz
toolz
はPythonのbuilt-inのitertools
やfunctools
を拡張してよりfunctionalに書けるようにするためのライブラリです。cytoolz
はそれをCythonで作りなおしてより高速化したもです。これらに実装されているpipe
とカリー化された関数群が非常に便利です。先ほどのひどいコードは次のように書けます。
from cytoolz.curried import *
import operator as O
result = pipe(range(0, 10001),
filter(lambda x: x % 3 == 0),
map(lambda x: x + 1),
reduceby(lambda x: x % 10, O.add),
lambda d: d.items(),
list
)
どうでしょう。1つ目の引数で処理したいデータを与え,2つ目以降の引数で適用したい関数を次々に与えます。R言語に詳しい方でしたらのdplyr
を連想するかもしれません。ちなみにここで使用しているfilter
,map
,reduceby
は全てカリー化されたもので,map(f, data)
がmap(f)(data)
のようにかけるのでこのようにpipe
で繋げられます。カリー化されたものを使用しない場合はpipe
をthread_last
に置き換えると各関数の最後の引数に前の関数で処理したデータを次々に渡していきます。
ScalaFunctional
scalafunctional
はその名の通り,Scalaのコレクション-likeに操作できるようにするライブラリです。そこまでするならScalaをつ(ryこのライブラリではseq
という専用のクラスにlist
やdict
等を入れてdotの連鎖で処理します。
from functional import seq
result = seq(range(0, 10001)) \
.filter(lambda x: x % 3 == 0) \
.map(lambda x: x + 1) \
.map(lambda x: (x % 10, x)) \
.reduce_by_key(O.add) \
.to_list()
これが最もScalaに近いですね。ただしPythonだと行末にバックスラッシュが必要になるので多少面倒です。あとはPythonのlambda
式はScalaの関数ほど柔軟ではないので複雑な処理の場合は一度関数をdef
する必要が出てくるかもしれません。いずれにせよ,非常にシンプルで美しいですね。
fn.py
fn.py
もPythonの関数型プログラミングのためのライブラリです。最大の特徴はScalaのプレースホルダーのような書き方ができることです。
from fn import _
result = map(_ + 1, range(10))
単純にlambda
の代わりに使用できます。
f = _ + 1
f(10)
>>>
11
toolz
やscalafunctional
との相性がいいです。
result = pipe(range(10),
map(_ + 1),
list
)
result = seq(range(10)) \
.map(_ + 1) \
.to_list()
ちなみんIPythonなどでは_
は最後の出力を表す予約後のようなのでそこで使用する際には別名でimport
する必要があります。
from fn import _ as it
まとめ
toolz
たscalafunctional
を使用すればPythonでも関数型プログラミングが捗ります。scalafunctional
はScalaのコレクション操作と全く同じように書けます。一方のtoolz
のpipe
を使用すればコレクション操作だけでなく,より汎用性のあるデータ処理フローを書くことができます。これらをfn.py
を上手く組み合わせてFunctional Python Lifeを満喫してください1。
今回使用したライブラリは全てGitHubで公開されています。もちろんPyPIにも登録されているのでpip
でインストールできます。
- Toolz : https://github.com/pytoolz/toolz
- CyToolz : https://github.com/pytoolz/cytoolz/
- ScalaFunctional : https://github.com/EntilZha/ScalaFunctional
- fn.py : https://github.com/kachayev/fn.py
-
定番のPandasでもDataFrameに対してある程度dot連鎖で処理を繋げられるようにもなっています。 ↩