「実践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.count
をc.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
の値として採用して吐き出す。
もしリターン値がセンチネル値に一致した場合、next
はStopIteration
を 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.")
for
の else
は完全に回りきっちゃったときにだけ実行される部分ですな
こう、なんか使えそうで使えなさそうでちょっと使えそうな感じですね。
センチネル値にちょうど一致しない限り止まってくれないので、もしその値を飛び越えちゃったら無限ループします。
でも、コーラブルオブジェクトがStopIteration
を上げてくれたときは、それをnext
が受け継いで吐いてくれるので止まります。
ただ、コーラブルオブジェクト内にそういう制御を書くなら(i.e. 元々イテレータ用にコーラブルオブジェクトを書くくらいなら)、ジェネレータ使ったほうが…って気もする…。
「いや、lambda: next(generator)
とセンチネル値で、途中で止まるようなイテレータを作れるじゃん!」
「itertools.takewhile
でよくない?」
10.1. itertools — 効率的なループ実行のためのイテレータ生成関数 — Python 3.5.2 ドキュメント
ジェネレータ使ってるなら、素直にif
でセンチネル値一致の分岐させたほうがいいと思うけど…。
あっ、これあれか、組み込みのイテレータプロトコルを使わない場合のイテレータの作り方に相当するのか?(とすれば、組み込みでイテレータプロトコルのある(さらにいえばもっと簡単にジェネレータまである)Pythonにはあんまり必要ないのかもしれない(もっと内部のところで使ってたりするのかな?))