Pythonのコレクション操作をメソッドチェーンでやる

以前このような記事を書いた.

dawn.hateblo.jp

詳しくはそちらを読んでいただくとして,Pythonのコレクション操作がイケてないという気持ちが僕にはある*1. しかし,Pythonには豊富な資産(numpy,pandas,networkx,scikit-learnなどなど...)があり,Pythonを使わざるをえないことがまれによくある.

上の記事でも書いたように,僕はRubyのコレクション操作のようにmapやfilterをメソッドチェーンするのが好きだ. Pythonでも同様のコレクション操作を実現できないか?というのがこの記事の主題である.

既存のクラスを拡張する

Pythonでは,既存のクラスに後からインスタンスメソッドを生やすことができる.

import networkx as nx

def hoge(self):
    return 'Extended!'

nx.Graph.my_special_method = hoge

G = nx.Graph()
G.my_special_method() # => 'Extended'

この調子で list クラスに map とか filter を生やせば,メソッドチェーンでコレクション操作が可能になりそうな雰囲気がする. しかし,Pythonには組み込みクラスにインスタンスメソッドを生やせないという制約がある. そして list クラスは組み込みクラスである. 従って, list クラスに map とか filter を生やすことはできない.

def map(self, func):
    return [func(elem) for elem in self]

list.map = map # => Error!

イテレーターをラップする新しいクラスを作る

既存のクラスの拡張が頓挫したので,別の手段を考える.

一番単純なのは,適当に新しいイテレーターのクラスを定義してやり,そのコンストラクタにイテレーターを渡してやることである.

class i(object):
    def __init__(self, iterator):
        self.iterator = iterator

    def map(self, func):
        return i(map(func, self.iterator))

    def filter(self, func):
        return i(filter(func, self.iterator))

    def to_list(self):
        return list(self.iterator)

i([2,1,15,-4,-5,-12]).map(lambda x: x*2).filter(lambda x: x <= 10).to_list()
# => [4, 2, -8, -10, -24]

つまり,Pythonの組み込みリストをメソッドチェーンが可能な世界に "持ち上げ" て操作を行い,最後に list として取り出してやればよいのである. このやり方は一応うまくいく.しかし,問題点もある.

問題点1:メソッドチェーン中に改行できない

Pythonではメソッドチェーン中に改行することができない.これは長いコレクション操作をするときにめんどくさいことになる.

i([1,2,3,4,4,5,6]).map(somefunc1)
                         .filter(somecondition) # => Syntax error
                         .map(somefunc2)       # => Syntax error

この問題は,行末にバックスラッシュ\を入れることで一応回避できる

i([1,2,3,4,4,5,6]).map(somefunc1)\
                         .filter(somecondition)\ # => Ok
                         .map(somefunc2)       # => Ok

\を入れ忘れたり消し忘れたりしてウォアアアーーとなることもあるが...

問題点2:持ち上げて戻すのが冗長

メソッドチェーンを3つぐらいつなげてると別に気にならないんですが,一回mapしたいだけのときとかだと冗長さが気になってくる.

i(lst).map(lambda x: x * 2).to_list() # 冗長!

結論

郷に入っては郷に従え

*1:異論は認める