Python でパイプ演算子を使いたい
ネタ記事です。/ This is a joke post which makes no practical sense.
はじめに
Python pandas
では主要な操作を以下のようにメソッドチェインの形で書くことができる。
# Python (pandas) df.assign(x=df['y'] + df['z']).groupby('x').sum()
pandas
v0.16.2 で DataFrame
と Series
に .pipe
というメソッドが追加され、このチェインを外部の関数/メソッドに対して連結できるようになった。利用例は以下のリンクを。
補足 matplotlib
でも v1.5.0 で ラベルデータ対応 が追加され、各関数が .pipe
から利用できるようになる予定。
このメソッドチェインによる "処理の連結" を、R ( {magrittr}
) のようにパイプ演算子 %>%
を使って統一的に書きたい。
# R (magrittr + dplyr) df %>% mutate(x = y + z) %>% group_by(x) %>% summarize_each(funs(sum))
R と異なり、 Python では自作の演算子を定義することはできない。Python の演算子の中では 右方向ビットシフト演算子 >>
がパイプ演算子っぽく見えなくもない。これを使って R のような動作をさせたい。
演算子のオーバーライド
まずはサンプルデータを作成する。
import numpy as np import pandas as pd pd.__version__ # '0.16.2' df = pd.DataFrame({'X': [1, 2, 3, 4, 5], 'Y': ['a', 'b', 'a', 'b', 'a']}) df # X Y # 0 1 a # 1 2 b # 2 3 a # 3 4 b # 4 5 a
普通に .pipe
を使うとこのような感じになる。
func = lambda x: x df.pipe(func) # X Y # 0 1 a # 1 2 b # 2 3 a # 3 4 b # 4 5 a
この処理を >>
演算子を使って書きたい。そのままでは、演算方法が未定義のため TypeError
となる。
df >> func
# TypeError: unsupported operand type(s) for >>: 'DataFrame' and 'function'
Python では各演算子に対応する処理はマジックメソッドとして実装する。そのため、>>
に対応する __rshift__
に .pipe
と同じ処理をさせればよい。また、R のように 右辺には関数を渡したいので、pandas
のメソッドを適当にラップする関数 pipify
を定義する。
pd.DataFrame.__rshift__ = pd.DataFrame.pipe def pipify(f): def wrapped(*args, **kwargs): return lambda self: f(self, *args, **kwargs) return wrapped head = pipify(pd.DataFrame.head)
できた。
df >> head(n=2) # X Y # 0 1 a # 1 2 b
groupby
も同じように書ける。集約関数は DataFrame
や DataFrameGroupBy
双方から使うため以下のように定義する。
groupby = pipify(pd.DataFrame.groupby) def sum(*args, **kwargs): return lambda self: self.sum(*args, **kwargs)
また、groupby
した結果からも >>
で演算を連結したい。そのためにDataFrameGroupBy.__rshift__
を定義する。
def _pipe(self, func): return func(self) pd.core.groupby.DataFrameGroupBy.__rshift__ = _pipe
できた。
df >> groupby('Y') >> sum() # X # Y # a 9 # b 6 df >> sum() # X 15 # Y ababa # dtype: object
右辺側クラスによるオーバーライド
DataFrame
が左辺にある場合、パイプ演算は上記のように定義できた。加えて、DataFrame
が右辺にある場合も同じようなことがしたい。
以下のような記法で、左辺の辞書から DataFrame
を作成したい。これも (当然) そのままではできない。
data = {'X': [1, 2, 3, 4, 5], 'Y': ['a', 'b', 'a', 'b', 'a']} data >> pd.DataFrame() # TypeError: unsupported operand type(s) for >>: 'dict' and 'DataFrame'
Python では、左辺側に 右辺を処理できるマジックメソッドがない場合は、右辺側のマジックメソッド ( DataFarme.__rrshift__
) が利用される。これを適当に定義すればよい。
def _get(a, b): return pd.DataFrame(b) pd.DataFrame.__rrshift__ = _get
できた。
data = {'X': [1, 2, 3, 4, 5], 'Y': ['a', 'b', 'a', 'b', 'a']} data >> pd.DataFrame() # X Y # 0 1 a # 1 2 b # 2 3 a # 3 4 b # 4 5 a
NumPy に対するオーバーライド
DataFrame
は np.ndarray
からも作成することができる。が、>>
ではうまく動かない。
data = np.array([[1, 2], [3, 4]]) pd.DataFrame(data) # 0 1 # 0 1 2 # 1 3 4 data >> pd.DataFrame() # TypeError: ufunc 'right_shift' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''
ndarray
のマジックメソッドは スカラーや ndarray
ライクな引数を汎用的に処理できるように定義されている。具体的には、引数が ndarray
のサブクラス、もしくは Array Interface ( __array__
) をもつとき、ndarray
によって処理が行われる。
DataFrame
は ndarray
のサブクラスではないが Array Interface をもつ。右辺が不正なため通常の例外が送出されてしまい、右辺側のマジックメソッドに処理が移らない。
参考 Subclassing ndarray — NumPy v1.11 Manual
issubclass(pd.DataFrame, np.ndarray) # False df.__array__() # array([[1, 'a'], # [2, 'b'], # [3, 'a'], # [4, 'b'], # [5, 'a']], dtype=object)
ここで DataFrame
側のマジックメソッドを利用したい場合、上記リンクに記載されている __array_priority__
プロパティを設定すればよい。DataFrame
側の優先度を上げると期待した処理が行われることがわかる。
pd.DataFrame.__array_priority__ = 1000 data >> pd.DataFrame() # 0 1 # 0 1 2 # 1 3 4
まとめ
R 使ったほうがいい。Use R!
補足 こんなパッケージもあります。
2016/10/18 追記
続き(?)です。