ゆくゆくは有へと

おかゆ/オカ∃/大鹿有生/彼ノ∅有生 の雑記

コーラブルオブジェクトをイテレータ化する

「実践Python3」の一部事項のメモ

__call__ メソッドを実装したクラスのインスタンスなり、関数なり、バウンドメソッドなり。コーラブルオブジェクトは呼び出し(とそのときに渡される引数)に応じて何か値を返したり、どっかの内部状態(IOとか自身の属性とか)を変えたりしてくれる。

普通はコーラブルオブジェクトをイテレータ化しようとは思わないというか、イテレータ化の目処が立たないですが、呼び出しによって自身の内部状態を変えながら値を返してくれるマン(カウンターのような)なら少しその気がありますね。

# coding=utf-8

class C:
    def __init__(self):
        self.i = 0

    def count(self):
        print("exec.", end=" ")
        self.i += 1
        return self.i

この c.countc.i が9になるまで回したいとき、まず思いつくのは for 文です:

c = C()

for _ in range(9):
    print(c.count())

ただ、これは「値を 9 まで回す」、ではなくて、「c.count を 9回実行する」なので、少し不安定です。

なら、whileを使えばいいような?

c = C()

while c.i != 9:
    print(c.count())

いい感じですが、i がプライベート属性だった場合はこの方法は使えません。ちょっと回りくどいですが、

c = C()
i = 0
while i != 9:
    i = c.count()
    print(i)

としてやれば確かに機能します。

結局、for では単純に回数しか、while では別変数や属性を用いて値を監視しながら回すことになります。

もっとこう、c.count の吐き出した値だけを見てよしなに回せたりしないかなあ?

c = C()
for elem in iter(c.count, 10):
    print(elem)

iter関数は引数を2つ取った場合、コーラブルイテレータをつくりだすそう。x1にコーラブルオブジェクトを、x2にセンチネル値(その値が返されたらイテレータを閉めるような値)を入れます。

type を見てみると、<class 'callable_iterator'> でした。

上の実行結果です:

exec. 1
exec. 2
exec. 3
exec. 4
exec. 5
exec. 6
exec. 7
exec. 8
exec. 9
exec.

コーラブルイテレータは、next で次の要素を吐き出す際に、まず第一引数に取ったコーラブルオブジェクトを呼び出す。

今回は c.count が呼び出され、print("exec.", end=" ")が実行されたあと、値が返される。

コーラブルオブジェクトが返した値をセンチネル値と比較し、等しくなければnextの値として採用して吐き出す。

もしリターン値がセンチネル値に一致した場合、nextStopIterationを raise する。

上の結果では、10回目のnextで呼び出されたコーラブルオブジェクトのprint文は実行されていますが、その返り値(10)はセンチネル値と等しいためにelemに渡されずイテレータが止まります。

2. 組み込み関数 — Python 3.5.2 ドキュメント より:

第二引数 sentinel が与えられているなら、 object は呼び出し可能オブジェクトでなければなりません。この場合に生成されるイテレータは、 __next__() を呼ぶ毎に object を引数無しで呼び出します。返された値が sentinel と等しければ、 StopIteration が送出され、そうでなければ、戻り値がそのまま返されます。

コーラブルオブジェクトはnextの度に引数無しで呼び出されるので、呼び出す際に何か値を渡したいというときにはもう少し工夫しないといけない。

イテレート中、同じ引数を与えたい場合

lambda: callable_obj(a, b, c) みたくすればOK。たとえばさっきのクラスに

def count_add(self, j):
    self.i += 1
    return self.i + j

を追加して、j=5イテレータを回したいとしたら、

for elem in iter(lambda: C().count_add(5), 9):
    ...

とすればいい。

イテレータ中、異なる引数を与えたい場合

いやそれもうその引数をリストにして for 文回したほうがよくない?

c = C()
for j in [1, 3, 2, 5, 2]:
    tmp = c.count_add(j)
    if tmp == 9:
        break
    else:
        print(tmp)
else:
    raise ValueError("not become 9.")

forelse は完全に回りきっちゃったときにだけ実行される部分ですな


こう、なんか使えそうで使えなさそうでちょっと使えそうな感じですね。

センチネル値にちょうど一致しない限り止まってくれないので、もしその値を飛び越えちゃったら無限ループします。

でも、コーラブルオブジェクトがStopIterationを上げてくれたときは、それをnextが受け継いで吐いてくれるので止まります。

ただ、コーラブルオブジェクト内にそういう制御を書くなら(i.e. 元々イテレータ用にコーラブルオブジェクトを書くくらいなら)、ジェネレータ使ったほうが…って気もする…。

「いや、lambda: next(generator) とセンチネル値で、途中で止まるようなイテレータを作れるじゃん!」

itertools.takewhile でよくない?」

10.1. itertools — 効率的なループ実行のためのイテレータ生成関数 — Python 3.5.2 ドキュメント

ジェネレータ使ってるなら、素直にifでセンチネル値一致の分岐させたほうがいいと思うけど…。

あっ、これあれか、組み込みのイテレータプロトコルを使わない場合のイテレータの作り方に相当するのか?(とすれば、組み込みでイテレータプロトコルのある(さらにいえばもっと簡単にジェネレータまである)Pythonにはあんまり必要ないのかもしれない(もっと内部のところで使ってたりするのかな?))