何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

Python Puzzler 「Puzzle 2: __iter__ VS. __getitem__」

Puzzle 2: __iter__ VS. __getitem__

イテラブルオブジェクトとは__iter__()または__getitem__()が定義されたクラスのことである。 次のプログラムでは半角英大文字をたどるイテレータを提供するイテラブルオブジェクトのクラスが定義されている。 以下を実行するとどのような出力が返されるか。

import string


class UpperCase(object):

    def __init__(self, first=None):
        self.index = -1 if first is None else string.ascii_uppercase.index(first) -1

    def __call__(self):
        self.index += 1
        if self.index < len(string.ascii_uppercase):
            return self.__getitem__(self.index)
        raise StopIteration()

    def __getitem__(self, index):
        if 0 <= index < 26:
            return string.ascii_uppercase[index]
        raise IndexError()

    def __iter__(self):
        return (c for c in reversed(string.ascii_uppercase))

uc = UpperCase('J')
print("".join(c for c in uc))
print("".join(c for c in iter(uc, 'N')))

Solution 2: __iter__ VS. __getitem__

出力結果は次のようになる。

ZYXWVUTSRQPONMLKJIHGFEDCBA
JKLM

この動作を理解する鍵は組み込み函数iter()である。 『Python文法詳解』の「7.3.1 イテラブルなオブジェクト」(P.258)には次のように説明してある。

iterable__iter__()メソッドを実装している場合は、呼び出してイテレータを取得します。 __iter__()がなく、__getitem__()メソッドを実装している場合は、__getitem__()を呼び出して値を返すイテレータを作成します。

つまり、print("".join(c for c in uc))__iter__()が実装してあったため、 __iter__()が呼び出され、__getitem__()は呼ばれなかったのである。

iter()はイテラブルオブジェクトではなく、呼び出し可能オブジェクトと番兵を与えてイテレータオブジェクトを作る方法も存在する。 公式ドキュメントから引用する。

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

つまり、print("".join(c for c in iter(uc, 'N')))とすると uc()が'N'が現れるかイテレータが尽きるまで繰り返し呼び出される。 また、__init__()においてfirstを渡すと指定した大文字からイテレートが開始するように細工してある。 よって、JKLMと出力される。