CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: pipe を使った Infix プログラミング

今回紹介する Python の pipe というサードパーティ製のパッケージは、実用性はともかくとして非常に面白い API になっている。 pipe を使ったソースコードを見れば目からウロコが落ちること請け合いだ。 その独特な書き方はインフィックス記法というらしい。

pypi.python.org

使った環境は次の通り。

$ 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&amp;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!

自身の左側にある演算子に反応するところがミソだね。