Pipeがおもしろかったのでどうやって作ってるのか調べてみた。

Pipe自体のコードの行数も少ないのでざっと眺めて見たところ以下のような感じだった。

  • Pipeの肝はPipeデコレータ。
  • __ror__を定義することで"|"の意味変えている。*1
  • すべての関数にPipeデコレータを付ける。

とかしているよう。Pipeデコレータの定義は以下のとおり。

class Pipe:
    """
    Represent a Pipeable Element :
    Described as :
    first = Pipe(lambda iterable: next(iter(iterable)))
    and used as :
    print [1, 2, 3] | first
    printing 1

    Or represent a Pipeable Function :
    It's a function returning a Pipe
    Described as :
    select = Pipe(lambda iterable, pred: (pred(x) for x in iterable))
    and used as :
    print [1, 2, 3] | select(lambda x: x * 2)
    # 2, 4, 6
    """
    def __init__(self, function):
        self.function = function

    def __ror__(self, other):
        return self.function(other)

    def __call__(self, *args, **kwargs):
        return Pipe(lambda x: self.function(x, *args, **kwargs))

とりあえず、

  • オペレータを再定義する(特に__ror__)とどうなるか?
  • 何でクラスベースのデコレータなのか?

がわかんない。なので調べてみる。

オペレータの再定義

__ror__を再定義したオブジェクトの振る舞いを調べてみる。
Boxクラスは"|"を実行すると、中のlistに値を追加するように定義したクラス。

class Box(object):
    def __init__(self, content=[]):
        self.content = 

    def __ror__(self, x):
        self.content.append(x)
        return self

    def pop(self):
        return self.content.pop

実行例は以下のとおり。

box = Box()
box = 40 | (30 | ( 20 | (10 | box)))
print box.content

# [10, 20, 30, 40]

## ちなみに
## 40 | 30 | 20 | 10 | boxは(((40 | 30) | 20) | 10) | boxと同様
## それは box.__ror__(10.__ror__(20.__ror__(30.__ror__(40))))

Pipeのことを考えると、実は重要なのはコメントの部分。
__ror__は上のような結合順序らしい。

__ror__の結合順序を考えて、"|"を書き換えてみる

Pipeを使った以下の例はこんな感じに書き換えられる。

from pipe import skip, concat, lineout

[1, 2, 3, 4] | skip(1) | concat | lineout
#は
(([1, 2, 3, 4] | skip(1)) | concat) | lineout
#でこれは、以下と同じ
lineout.__ror__(concat.__ror__(skip(1).__ror__([1, 2, 3, 4])))

# 2, 3, 4
# 2, 3, 4
# 2, 3, 4

ちょっと分かりにくいけれど、"x | f | g | h"というのがおおよそ"h(g(f(x)))"に対応してくれる感じ。
これでかっこを減らしている。
ただ、ここでのf, g, hは普通の関数ではなく、Pipeデコレータを適用した関数。

Pipeデコレータはクラスベースのデコレータ

試しに関数デコレータでは無理か考えてみる。
__ror__の無いデコレータを関数デコレータで作ると以下のような感じ。
mypipeという名前で作ってみた。

def mypipe(function):
    def _mypipe(*args, **kw):
        return mypipe(lambda x : function(x, *args, **kw))
    return _mypipe

def add(x, y):
    return x + y

print mypipe(add)
print mypipe(10)
print mypipe(10)(20)
print mypipe(10)(20)(30)

# <function _mypipe at 0x7fd4db49c5f0>
# <function _mypipe at 0x7fd4db49c5f0>
# <function _mypipe at 0x7fd4db49c6e0>
# <function _mypipe at 0x7fd4db49c758>

mypipeを適用した関数を実行しようとすると、またmypipeで再帰している。
なので、実行まであとちょっとというまま遅延してしまう。
このままでは実行できない。

クラスベースのデコレータにすると、複数の振る舞いを持てるようになる。
なので、クラスベースのデコレータに変える必要があるよう。

クラスベースのデコレータに書き換えて、bindというメソッドを持たせた。
あと、Pipeで定義されている関数を新しく作ったMyPipデコレータを使って書き直した。

class MyPipe:
    def __init__(self, function):
        self.function = function

    def bind(self, other):
        return self.function(other)

    def __call__(self, *args, **kwargs):
        return MyPipe(lambda x: self.function(x, *args, **kwargs))

@MyPipe
def skip(iterable, qte):
    "Skip qte elements in the given iterable, then yield others."
    for item in iterable:
        if qte == 0:
            yield item
        else:
            qte -= 1

import sys
@MyPipe
def concat(iterable, separator=", "):
    return separator.join(map(str,iterable))

@MyPipe
def lineout(x):
    sys.stdout.write(str(x) + "\n")

lineout.bind(concat.bind(skip(1).bind([1, 2, 3, 4])))
# 2, 3, 4

これを上の__ror__での結合順序と合わせてPipeができている。

まとめ

pipeは以下の3つの機能を用いて作られている。

  • __ror__を定義することで"h(g(f(x)))"という関数呼び出しを"x | f | g | h"と書けるようになる。
  • Pipeデコレータによって,itertoolsの関数などのイテレータに対する関数を"h(g(f(iterator)))"と書けるようにしている。
  • クラスデコレータによって、デコレータの適用対象に複数の振る舞いを持たせている

こういうの思いつくのすごいなー。

*1:他にも色々、組み込みの演算子を上書きするメソッドがあります。この辺見る。http://docs.python.org/reference/datamodel.html#emulating-numeric-types