LoginSignup
24
21

More than 5 years have passed since last update.

PythonでScala-likeにコレクション操作を行う

Posted at

Background

Scalaのコレクション操作かっこいいですよね。余計な中間変数とか作らずにすっきり書けます。

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でやろうとすると...

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)

見難くて目も当てられません。ちなみに中間変数を使用せずに一発で書くと

Python
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)))のように逆の順番に書かないといけないのが原因です。

実はこれを解決するライブラリがあるんです。そうtoolzscalafunctionalfn.pyならね。この記事ではScalaで書けなど意見はNGワードです。

Toolz, CyToolz

toolzはPythonのbuilt-inのitertoolsfunctoolsを拡張してより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で繋げられます。カリー化されたものを使用しない場合はpipethread_lastに置き換えると各関数の最後の引数に前の関数で処理したデータを次々に渡していきます。

ScalaFunctional

scalafunctionalはその名の通り,Scalaのコレクション-likeに操作できるようにするライブラリです。そこまでするならScalaをつ(ryこのライブラリではseqという専用のクラスにlistdict等を入れて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

toolzscalafunctionalとの相性がいいです。

toolz
result = pipe(range(10),
    map(_ + 1),
    list
)
scalafunctional
result = seq(range(10)) \
    .map(_ + 1) \
    .to_list()

ちなみんIPythonなどでは_は最後の出力を表す予約後のようなのでそこで使用する際には別名でimportする必要があります。

from fn import _ as it

まとめ

toolzscalafunctionalを使用すればPythonでも関数型プログラミングが捗ります。scalafunctionalはScalaのコレクション操作と全く同じように書けます。一方のtoolzpipeを使用すればコレクション操作だけでなく,より汎用性のあるデータ処理フローを書くことができます。これらをfn.pyを上手く組み合わせてFunctional Python Lifeを満喫してください1

今回使用したライブラリは全てGitHubで公開されています。もちろんPyPIにも登録されているのでpipでインストールできます。


  1. 定番のPandasでもDataFrameに対してある程度dot連鎖で処理を繋げられるようにもなっています。 

24
21
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
21