以前このような記事を書いた.
詳しくはそちらを読んでいただくとして,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:異論は認める