今回紹介する Python の pipe というサードパーティ製のパッケージは、実用性はともかくとして非常に面白い API になっている。 pipe を使ったソースコードを見れば目からウロコが落ちること請け合いだ。 その独特な書き方はインフィックス記法というらしい。
使った環境は次の通り。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.11.3 BuildVersion: 15D21 $ python --version Python 2.7.11
インストール
まずは pip で pipe をインストールする。
$ pip install pipe
使ってみる
ここからは Python の REPL で pipe の動作を試していく。
$ python
まずは pipe の提供する API をすべて読み込んでおこう。
>>> from pipe import *
pipe を使うときは、その名の通りパイプ演算子を駆使することになる。 基本は Iterable なオブジェクトに対してパイプ演算子をつなげて、そこに何らかの操作を続ける。
add
例えばリストの内容をすべて足したいときはパイプ演算子に続けて add を使う。
>>> [1, 2, 3] | add 6
where
where を使えば特定の条件にマッチする内容だけが得られる。 結果が複数のときは基本的に Iterable なオブジェクトが返る。
>>> [1, 2, 3] | where(lambda x: x % 2 == 0) <generator object <genexpr> at 0x100d33dc8>
分かりやすいように結果をリストにしてみよう。 これも as_list をつなげることで変換できる。
>>> [1, 2, 3] | where(lambda x: x % 2 == 0) | as_list [2]
stdout / lineout
内容を標準出力に書き出すには stdout や lineout が使える。 stdout は末尾に改行文字をつけないやつで lineout はつけるやつ。
>>> 'foo\n' | stdout foo >>> 'foo' | lineout foo >>>
tee
Unix のコマンドでもおなじみの tee を使えば標準出力に出しつつ、次のパイプにつなげることができる。
>>> [1, 2, 3] | tee | as_list 1 2 3 [1, 2, 3]
as_list
既に登場してるけどオブジェクトをリストに変換するときは as_list を使えば良い。
>>> (1, 2, 3) | as_list [1, 2, 3]
as_tuple
同様にタプルへの変換なら as_tuple を使う。
>>> [1, 2, 3] | as_tuple (1, 2, 3)
as_dict
辞書なら as_dict だ。
>>> [('a', 1), ('b', 2), ('c', 3)] | as_dict {'c': 3, 'b': 2, 'a': 1}
concat
Iterable な内容をつなげて文字列にするときは concat を使う。 つなげるのに使う文字を引数で指定することもできる。
>>> [1, 2, 3] | concat '1, 2, 3' >>> [1, 2, 3] | concat('&') '1&2&3'
average
算術平均を取りたいときは average が使える。
>>> [1, 2, 3] | average 2.0
netcat
netcat を使えば文字列をリモートホストに送ることもできる。 試しに Google に HTTP リクエストを投げてみよう。
>>> 'GET / HTTP/1.0\n\n' | netcat('www.google.jp', 80) | concat 'HTTP/1.0 302 Found\r\nCache-Control: private\r\nContent-Type: text/html; charset=UTF-8\r\nLocation: http://www.google.co.jp/?gfe_rd=cr&ei=KRTDVqXZNYug8wfQmrD4Dw\r\nContent-Length: 261\r\nDate: Tue, 16 Feb 2016 12:20:57 GMT\r\nServer: GFE/2.0\r\n\r\n<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">\n<TITLE>302 Moved</TITLE></HEAD><BODY>\n<H1>302 Moved</H1>\nThe document has moved\n<A HREF="http://www.google.co.jp/?gfe_rd=cr&ei=KRTDVqXZNYug8wfQmrD4Dw">here</A>.\r\n</BODY></HTML>\r\n'
上手くいった。
netwrite
netcat は書き込みと読み込みを両方やる命令だったけど書き込むだけなら netwrite で良い。
まずは、動作確認のために socat を使ってエコーサーバを立てておこう。
$ brew install socat
$ socat -v tcp-listen:8080,fork system:cat
そして REPL で netwrite を使ってエコーサーバに文字列を送り込む。
>>> 'Hello, World!\n' | netwrite('localhost', 8080)
するとエコーサーバのターミナルで文字列が送られたことが確認できる。
$ socat -v tcp-listen:8080,fork system:cat > 2016/02/16 21:26:13.875238 length=14 from=0 to=13 Hello, World! < 2016/02/16 21:26:13.879153 length=14 from=0 to=13 Hello, World!
count
要素の数を得るには count が使える。
>>> [1, 2, 3] | count 3
first
最初の要素を得るには first を使う。
>>> [1, 2, 3] | first 1
chain
ネストしたリストをフラットにするには chain が使える。
>>> [[1, 2], [3, 4]] | chain | as_list [1, 2, 3, 4]
traverse
深くネストしているときは traverse を使う。
>>> [1, [[2], [3]]] | traverse | as_list [1, 2, 3]
select
各要素に何らかの処理を適用するには select を使う。 これは組み込み関数の map() に相当する。
>>> [1, 2, 3] | select(lambda x: x * x) | as_list [1, 4, 9]
where
特定の条件にマッチする要素だけ得るときは where を使う。 組み込み関数の filter() に相当するかな。
>>> [1, 2, 3] | where(lambda x: x % 2 == 0) | as_list [2]
take_while
特定の条件にマッチするまで要素を得るときは take_while を使う。 これは itertools.takewhile() に相当する。
>>> [1, 2, 3] | take_while(lambda x: x < 3) | as_list [1, 2]
skip_while
同じように特定の条件にマッチする要素を飛ばすには skip_while を使う。 これは itertools.dropwhile() に相当する。
>>> [1, 2, 3] | skip_while(lambda x: x % 2 != 0) | as_list [2, 3]
chain_with
オブジェクトに要素をつなげたいときは chain_with を使う。
>>> [1, 2, 3] | chain_with([4], [5, 6], [7]) | as_list [1, 2, 3, 4, 5, 6, 7]
take
オブジェクトの先頭の要素だけ取り出したいときは take を使う。
>>> [1, 2, 3] | take(2) | as_list [1, 2]
tail
同じように末尾の要素だけほしいときは tail を使う。
>>> [1, 2, 3] | tail(2) | as_list [2, 3]
skip
先頭 n 個の要素から後ろがほしいときは skip を使う。
>>> [1, 2, 3, 4, 5] | skip(2) | as_list [3, 4, 5]
islice
要素の中からスライスを取り出したいときは islice を使う。 これは itertools.islice() に相当する。
例えば先頭だけを取り出したいときは引数をひとつだけ指定する。
>>> [1, 2, 3, 4, 5] | islice(2) | as_list [1, 2]
ふたつ指定すると開始と終了の場所が指定できる。
>>> [1, 2, 3, 4, 5] | islice(2, 4) | as_list [3, 4]
みっつ指定すれば開始、終了に加えて n 個間隔で要素を取り出せる。
>>> [1, 2, 3, 4, 5] | islice(0, None, 2) | as_list [1, 3, 5]
izip
ふたつの Iterable な要素をタプルにして取り出せるようにするには izip を使う。 これは itertools.izip() に相当する。
>>> [1, 2, 3] | izip(['a', 'b', 'c']) | as_list [(1, 'a'), (2, 'b'), (3, 'c')]
aggregate
要素をひとつずつ順番に処理してひとまとめにするときは aggregate を使う。 これは functools.reduce() に相当する。
>>> [1, 2, 3] | aggregate(lambda l, r: l * r) 6
any
特定の条件にヒットする要素が含まれるかを調べるときは any が使える。
>>> [1, 2, 3] | any(lambda x: x > 2) True
all
同じようにすべての要素が特定の条件にヒットするか調べるには all を使う。
>>> [1, 2, 3] | all(lambda x: x > 2) False
max
Iterable なオブジェクトの中で最も大きなものを得るには max を使う。
>>> [1, 2, 3] | max 3
min
同じように最小なら min を使う。
>>> [1, 2, 3] | min 1
groupby
条件によってオブジェクトをグループに分けたいときは groupby を使う。 例えば偶数と奇数でグループ分けしてみよう。
>>> [1, 2, 3, 4] | groupby(lambda x: x % 2 == 0) | as_list [(False, <itertools._grouper object at 0x1095a3390>), (True, <itertools._grouper object at 0x1095a3510>)]
そのままだと中身がジェネレータのままで見づらいのでリストに直す。 うん、かなり読みづらい。
>>> [1, 2, 3, 4] | groupby(lambda x: x % 2 == 0) | select(lambda x: x[1] | as_list) | as_list [[1, 3], [2, 4]]
sort
内容をソートするには sort が使える。
>>> [1, 5, 2, 4, 3] | sort [1, 2, 3, 4, 5]
ソートのやり方を指定するには key 引数を指定する。 これは sorted() の key と同じこと。
>>> ['This', 'is', 'a', 'pen'] | sort(key=str.lower) ['a', 'is', 'pen', 'This']
reverse
Iterable な内容をひっくり返すなら reverse を使う。
>>> [1, 2, 3] | reverse | as_list [3, 2, 1]
permutations
繰り返しを許さない順列を得るには permutations を使う。
>>> 'abc' | permutations(2) | as_list [('a', 'b'), ('a', 'c'), ('b', 'a'), ('b', 'c'), ('c', 'a'), ('c', 'b')]
自作のパイプを作る
pipe は自分でパイプに使う API を定義することもできる。 今回は試しに要素をランダムにシャッフルして返すものを作ってみよう。
pipe の API を作るには関数を @Pipe デコレータで修飾する。 関数はひとつの引数を取って値を返す。 引数はパイプの左側から渡されるオブジェクトになる。 そして返り値はパイプの右側に渡されるオブジェクトになる。
>>> import random >>> @Pipe ... def shuffle(x): ... random.shuffle(x) ... for i in x: ... yield i ...
実際に使ってみよう。 リストに対して自作した shuffle を使うとジェネレータが返る。
>>> [1, 2, 3, 4] | shuffle <generator object shuffle at 0x109732730>
このままだと分かりにくいのでリストに直そう。
>>> [1, 2, 3, 4] | shuffle | as_list [4, 3, 1, 2]
ばっちり中身がシャッフルされている。
まとめ
ここまで見てきたように pipe を使うととても独創的な記法で処理が記述できる。 あなたはもう pipe の魅力にメロメロのはずだ。 明日からでも本番のコードで使いたくなったに違いない。
いえ、わたしは遠慮しておきます。
補足
ちなみに、こうした API は Python がパイプ演算子の挙動をオーバーライドできるために実現できる。
すごく単純なサンプルを書いてみよう。 具体的にはユーザ定義クラスで特殊メソッド __ror__() を実装する。
>>> class Print(object): ... def __ror__(self, obj): ... print(obj) ...
これで Print クラスのインスタンスにパイプ演算子を使ったときに __ror__() メソッドが呼ばれる。
>>> 'Hello, World!' | Print()
Hello, World!
自身の左側にある演算子に反応するところがミソだね。
スマートPythonプログラミング: Pythonのより良い書き方を学ぶ
- 作者: もみじあめ
- 発売日: 2016/03/12
- メディア: Kindle版
- この商品を含むブログを見る