Python のイテレータとは


f:id:domodomodomo:20171126155731j:plain
図. イテレータのイメージ



イテレータとは、list, tuple, set などの集合を表現するオブジェクトから
iter 関数で生成された集合のコピーみたいなものだと考えてください。

イテレータから next 関数で要素を取り出すことができ
取り出す操作は for 文で自動的に繰り返す(iterate する)ことができます。





f:id:domodomodomo:20180113154538j:plain


何を言ってるのか、さっぱりだと思います。
まず、用途と使い方を先に、ざっくり見ておきたいと思います。


1. イテレータを理解すると何ができるようになるの?

答え: 自分で定義したクラスのオブジェクトを...

1. for 文の in で使えるようになったり
2. 集合を引数に取る関数で使えるようになったりします。


◯ 出来ること1: for 文の in で使えるようになる。

例えばチームを表現するクラスがあったとします。

class Team(object):
    def __init__(self):
        self.member_list = []



このように list を参照して for 文を回していたのが

>>> for member in team.member_list:
...     print(member)
川島 永嗣
香川 真司
長谷部 誠 
>>>



こんな風に in の中に自分が定義したクラスのオブジェクトが書けるようになります。

>>> for member in team:
...     print(member)
川島 永嗣
香川 真司
長谷部 誠 
>>>



iterable ... 上記のように for 文の in に直接代入できるオブジェクトを、 iterable なオブジェクトと言います。

◯ 実装の仕方

実装の仕方は、とっても簡単で、次のような iterator を返す __iter__ メソッドを追加するだけでできるようになります。

class Team(object):
    def __init__(self):
        self.member_list = []

    def __iter__(self):
        # iter 関数:
        #   list, set, dict などを引数にとり
        #   iterator を返す組み込み関数
        return iter(self.member_list)


samurai_brue = Team()
samurai_brue.member_list.extend(
    ['川島 永嗣', '香川 真司', '長谷部 誠'])

for member in samurai_brue:
    print(member)


◯ 出来ること2: 集合を引数に取る関数で使えるようになる。

set 関数を用いて差集合、和集合を取ったり , max 関数を用いて集合の最大値を取ったりすることもできるようになったりもします。

こんな風に書いていたのを

>>> set(team_a.member_list) - set(team_b.member_list) 



こんな風に書き換えたりもできたりします。

>>> set(team_a) - set(team_b) 



他にも iterable を引数に取る関数が使えるようになります。 公式マニュアルの組み込み関数のページ で ctrl+F で iterable で検索するといくつか引っかかってきます。例えば all, any, dict, enumerate, min, sorted, sum, tuple, zip 関数で使えます。

◯ この後の章立て

ここまで、イテレータの使い方を説明しました。イテレータを使えば、表現を簡潔にして for 文でグイグイ回せるようになるんだってこと、使い方だけ、とりあえず知っておけば問題ないと思います。

もし興味がわいたら、このあとも読み進めて見てください。このあとは、次のような具合で説明を進めていきます。


2 ~ 5 章...実際にイテレータを触ってみる。
6 ~ 9 章...4つのイテレータを自作する。
10 章...イテレータのメリットとデメリットを把握する。
11, 12 章...2種類あるイテレータの構造を把握する。
13, 14 章...用語を復習する。
15 ~ 18 章...もう少し正確にイテレータを把握する。



2. Pythonイテレータの動作

イテレータの動作を図で見てみる。

動作を図示するとこんな感じです。
ざっくり、てきとーに眺めてください。
f:id:domodomodomo:20171126155706j:plain

イテレータは、list, tuple, set などの集合を表現するオブジェクトから
iter 関数で生成された集合のコピーみたいなものだと考えてください。
f:id:domodomodomo:20171126155710j:plain

イテレータから1つ1つ要素を取り出すには
next 関数を使います。
f:id:domodomodomo:20171126155713j:plain
f:id:domodomodomo:20171126155716j:plain
f:id:domodomodomo:20171126155719j:plain
f:id:domodomodomo:20171126155722j:plain
f:id:domodomodomo:20171126155726j:plain

使い終わると
イテレータは空っぽになりますが
リストはそのままです。
f:id:domodomodomo:20171126183233j:plain


全体像は、こんな感じになります。
f:id:domodomodomo:20171126155731j:plain


イテレータの動作をコードで見てみる。

イテレータは、list, tuple, set などの集合を表現するオブジェクトから
iter 関数で生成された集合のコピーみたいなものだと考えてください。

>>> # list, tuple, set などの集合を表現する
>>> # オブジェクトを一般に container 総称します。
>>> container = [1, 2, 3, 4]
>>>
>>> iterator = iter(container)
>>> iterator
<list_iterator object at 0x10db73f60>
>>>
>>> # iterator は集合をコピーしたものなので
>>> # container と iterator の中身は同じ
>>> list(iterator)
[1, 2, 3, 4]


1. next 関数は、イテレータから1つ1つ要素を取り出すことができます。
2. next 関数は、イテレータが空ならば例外 StopIteration を送出します。

>>> next(iterator)
1
>>> next(iterator)
2
>>> next(iterator)
3
>>> next(iterator)
4
>>> next(iterator)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

◯ どのオブジェクトなら iter 関数を使って iterator を生成できるの?

for ~ in ... の in ... に入れることができるオブジェクトです。このようなオブジェクトは、for 文の中で繰り返すことができる (iterate できる) という意味で iterable なオブジェクトと呼ばれます。

3. for 文と一体何の関係があるの?

答え: for 文の中で next(iterator) が繰り返し呼び出されています。

next 関数を4回も手打ちさせるコードを見せられると、こんなの一体どこで使うんや.. とか、なんていうキチガイフレンズなのかな.. みたいな気分になってしまうかも知れません。

ここでは 4, 5 で手書きで実行した next 関数を空になるまで実行するように while 文に書き換え、それをさらに for 文に書き換えていきます。

Step1. while 文で書き換え

4, 5 では iterator が空になるまで手書きで next 関数をひたすら実行しました。あまり iterate 繰り返している感じがしないので while 文で書き換えて見ましょう。

>>> container = [1, 2, 3, 4]
>>> 
>>> iterator = iter(container)
>>> while True:
...     next(iterator)
... 
1
2
3
4
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
StopIteration
>>> 

Step2. 例外 StopIteration を捕まえる

都度、例外を投げられては実際の運用で使い物になりません。そこで、例外を捕まえます。

>>> container = [1, 2, 3, 4]
>>> 
>>> iterator = iter(container)
>>> while True:
...     try:
...         next(iterator)
...     except StopIteration:
...         break
1
2
3
4
>>> 

Step3. next の返り値を変数 element に代入させます。

iterator の返り値を再利用しやすくなりました。

>>> container = [1, 2, 3, 4]
>>> 
>>> iterator = iter(container)
>>> while True:
...     try:
...         element = next(iterator)
...     except StopIteration:
...         break
...
...     element
...
1
2
3
4
>>> 

Step4. for 文に書き換え

しかし Step3 のコードは、非常に長く煩雑です。何とかならないでしょうか。実は Python ではこのコードを、for 文を使って次のように書き換えることができます。

>>> container = [1, 2, 3, 4]
>>>
>>> for element in container:
...     # element = next(iterator) 
...     # が繰り返されている(iterate されている)。
...     element
... 
1
2
3
4
>>>

for 文は while 文よりも、簡潔に表現できます。for 文の中で element = next(iterator) という処理が、文字通り繰り返されている(iterate されていいる)ことがわかります。

また、一応 for 文と while 文の速度比較をして見たのですが、時と場合によって速さが違い、一概にどっちが速いとは言えなさそうです。
3. for 文と while 文の速度の比較

これを見ていると、もし next 関数と iterator 関数を実装することができれば、自分で作ったクラスでも for 文の in の中で使えそうですね。


4. next 関数, iter 関数を実装したい

実は next 関数と iter 関数の動作を実装できます。何故なら iter 関数も next 関数も、iterable なオブジェクトの __iter__ メソッド, __next__ メソッドをそれぞれ呼び出しているだけだからです。

◯ __iter__ メソッド

>>> # list, tuple, set などの集合を表現する
>>> # オブジェクトを一般に container 総称します。
>>> container = [1, 2, 3, 4]
>>>
>>> # iterator = iter(container)
>>> iterator = container.__iter__()
>>> iterator
<list_iterator object at 0x10db73f60>
>>>
>>> list(iterator)
[1, 2, 3, 4]

◯ __next__ メソッド

>>> # next(iterator)
>>> iterator.__next__()
1
>>> iterator.__next__()
2
>>> iterator.__next__()
3
>>> iterator.__next__()
4
>>> # 要素が空なら例外 StopIteration を送出
>>> iterator.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

◯ 疑問: なんでメソッドと関数があるの?

答え: 使い分けています。ユーザが自分で iterator を定義したいときは __iter__, __next__ メソッドから定義します。実際に使うときは iter, next 関数から呼び出します。

__iter__, __next__ メソッドと iter, next 関数が取る引数の違いに注目してください。

iter, next 関数は、__iter__, __next__ メソッドと異なり optional な引数を取ります。iter, next 関数は、単純に __iter__, __next__ メソッドを実行するだけでなく optional な引数を取り、それに基づいて異なる処理をします。

optional な引数に基づく iterator に共通する処理は、組み込み関数が担ってくれるというわけです。

この後は、__iter__, __next__ メソッドを実装して iterable な container と iterator を自作していくことになります。

5. container と iterator の関係

いままで触ってきた内容を元に、実装したいクラス、メソッドを図に落とすと次のようになります。
f:id:domodomodomo:20171126131131j:plain

container と iterator の関係

◯ 実装の方針

この図を見ると自分が作った container を for 文の in にいれたい場合は、container に __iter__ メソッドを追加して、iterator には __next__ メソッドを実装さえしてしまえば良さそうですね。

ここまでは iterator を触って大体の動作を把握しました。ここから先は次の4つの iterator を自作することを通して、最終的にジェネレータまで理解していきたいと思います。


6 章...iterator を自作する1 空集合
7 章...iterator を自作する2 リスト
8 章...iterator を自作する3 2分探索木
9 章...iterator を自作する4 2分探索木(ジェネレータ)

6. iterator を自作する1 空集合

やっと自作するところまでたどり着きました。空集合とか、気取って書いて見ましたが、何も要素を持たない iterator と言うことです。

◯ 目標

このクラスを

class Container(object):
     pass



for 文の in に使えるようにします(iterable にします)。最も小さい iterable なオブジェクトを実装していきます。

>>> for element in Container():
...     print(element)
>>> # 何も起こらない。とにかくエラーが発生しないことを目標に。

◯ 方針

公式のマニュアルを読みながら、実装を進めて見たいと思います。

Python はコンテナでの反復処理の概念をサポートしています。この概念は 2 つの別々のメソッドを使って実装されています; これらのメソッドを使ってユーザ定義のクラスで反復を行えるようにできます。
4.5. イテレータ型

Step1. container オブジェクト

まず iterate させたい値を持つ container オブジェクトに対して iterator オブジェクトを返す __iter__ メソッドを定義する。


コンテナオブジェクトに反復処理をサポートさせるためには、
以下のメソッドを定義しなければなりません。

container.__iter__()
イテレータオブジェクトを返します。

Step2. iterator オブジェクト

次に iterator オブジェクトには、次の2つのメソッドを定義する。イテレータオブジェクト自身を返す __iter__ メソッド と、次の要素を返す __next__ メソッド。


イテレータオブジェクト自体は
以下の 2 つのメソッドをサポートする必要があります。

iterator.__iter__()
イテレータオブジェクト自体を返します。

iterator.__next__()
コンテナの次のアイテムを返します。もしそれ以上アイテムが無ければ StopIteration 例外を送出します。

Step3. 図を更新

イテレータオブジェクト自体を返す iterator.__iter__() と言うのがメソッドが新しく登場してきました。ちょっと、図を更新してみます。
f:id:domodomodomo:20171126131853j:plain

container と iterator の関係2

◯ 実装

class Container(object):
    pass
    
    def __iter__():
          return Iterator()


class Iterator(object):
    def __iter__(self):
          return self

    # 要素はないので呼び出された
    # 瞬間に StopIteration を投げる。
    def __next__(self):
         raise StopIteration


if __name__ == '__main__':
    for element in Container():
        print(element)

疑問: なぜ iterator には自分自身を返すメソッド __iter__ を実装するの?

答え: わかりません。

自身を返す __iter__ を実装すると iterator 自身も iterable にはなりますが、それがメリットなのかと聞かれると疑問です。実は iterator に __iter__ メソッドがなくても for 文自体は全く問題なく動作します。

全く問題なく動作するので、マニュアルの誤記かなとも思ったのですが、組み込み型である list の iterator の list_iterator, dict の iterator の dict_keyiterator, str の iterator の str_iterator は、ちゃんと __iter__ メソッドを実装しています。

>>> # 1) list_iterator
>>> type(iter([]))
<class 'list_iterator'>
>>> hasattr(iter([]), '__iter__')
True
>>> 
>>> # 2) dict_keyiterator
>>> type(iter({}))
<class 'dict_keyiterator'>
>>> hasattr(iter({}), '__iter__')
True
>>>
>>> # 3) str_iterator
>>> type(iter(''))
<class 'str_iterator'>
>>> hasattr(iter(''), '__iter__')
True


あるいは「この定義なら container と iterator を1つのオブジェクトで実装できるようになるから(後述させていただく内部イテレータも同時に定義できるから)。」と言うのが理由かなとも思ったのですが、別にこの定義がで無くても内部イテレータを定義できているような気がして、いまいち決め手にかける感じがします。

7. iterator を自作する2 リスト

◯ 目標

tuple を属性に持つクラスを iterable にして
for 文で使えるようにしましょう。

#  このままでは for 文で使えない,  iterable でない
class Container(object):
    def __init__(self, *args):
        # type(args) is tuple
        self.tuple = args

*args って何?

for 文内で iterator が実行されると
文字列に Hello を付与する処理を
繰り返す(iterate)するように実装して見ましょう。

>>> container = Container(
... 'Yaruo', 'Yaranaio', 'Yarumi')
>>> 
>>> for element in container:
...     print(element)
... 
Hello, Yarumi.
Hello, Yaranaio.
Hello, Yaruo.



◯ 方針

Iterator には list を使用します。tuple は list.pop のような要素を一つずつ取り出す関数ないしメソッドがないからです。

◯ 実装

実装するとこんな感じになります。

class Container(object):
    def __init__(self, *args):
        # type(args) is tuple
        self.tuple = args

    # container.__iter__()
    def __iter__(self):
        return Iterator(*self.tuple)


class Iterator():
    def __init__(self, *args):
        # tuple は immutable で pop させてくれないので
        # list に変換する
        self.list = list(args)

    # iterator.__iter__()
    def __iter__(self):
        return self

    # iterator.__next__()
    def __next__(self):
        if self.list:
            return "Hello, " + self.list.pop() + "."
        # シーケンスが空であれば終了
        else:
            raise StopIteration('No name is stocked...')

if __name__ == "__main__":
    container = Container('Yaruo', 'Yaranaio', 'Yarumi')
    for element in container:
        print(element)



◯ "全ての要素を使い切ったとき" の判定は、どうやるの?

答え: 自作の iterator を止めるには StopIteration を吐くようにするしか実装方法はなさそうです。

マニュアルに "全ての要素を使い切ったとき (シーケンスが空であったりイテレータが StopIteration 例外を送出したなら、即座に)、 ... 中略 ... ループは終了します" と、しれっと書いてあったので

for 文は、シーケンス (文字列、タプルまたはリスト) や、その他の反復可能なオブジェクト (iterable object) 内の要素に渡って反復処理を行うために使われます:

for_stmt ::= "for" target_list "in" expression_list ":" suite
              ["else" ":" suite]

式リストは一度だけ評価され、これはイテラブルオブジェクトを与えなければなりません。 ... 中略 ... 全ての要素を使い切ったとき (シーケンスが空であったりイテレータが StopIteration 例外を送出したなら、即座に)、 ... 中略 ... ループは終了します

8. 複合文 (compound statement) — Python 3.6.3 ドキュメント



実は Python の for 文はこんな感じで、内部的に表現されてるのかなと期待しました。

lst = [1, 2, 3, 4]
iterator = iter(lst)

# 1) "シーケンスが空であったり"
# while iterator.__len__()
while iterator:
    try:
        e = next(iterator)
    # 2) イテレータが StopIteration 例外を送出したなら
    except StopIteration:
        break
    print(e)


# while iterator の評価のされ方
# -> while bool(iterator)
# -> while iterator.__bool__()
# -> while iterator.__len__()

# 組み込み関数の bool
#   iterator.__len__() が呼び出されます。
#   https://docs.python.org/ja/3/reference/datamodel.html#object.__bool__


"シーケンスが空であったり" するかどうかを伝えるために __len__ メソッド を実装したら伝えられるかなと思って実装しましたが

class Container(object):
    def __init__(self, *args):
        self.args = args

    def __iter__(self):
        # return map(lambda a: "Hello, " + a + ".", self.args)
        return Iterator(*self.args)

# StopIteration を吐かない iterator 
# 代わりに __len__ メソッドを実装。
class Iterator(object):
    def __init__(self, *args):
        self.args = list(args)

    def __len__(self):
        return len(self.args)

    def __iter__(self):
        return self

    def __next__(self):
        return "Hello, " + self.args.pop() + "."


iterator は、鮮やかに駆け抜け止まりませんでした (´;ω;`)ブワッ

>>> # pop が要素回数以上実行されている..
>>> for e in IterableContainer('Yaruo', 'Yaranaio', 'Yarumi'): print(e)
Hello, Yarumi.
Hello, Yaranaio.
Hello, Yaruo.
Traceback (most recent call last):
    for e in IterableContainer('Yaruo', 'Yaranaio', 'Yarumi'):
    return "Hello, " + self.args.pop() + "."
IndexError: pop from empty list



8. iterator を自作する3 2分探索木

もともと for 文で回せる tuple を属性にくっつけただけのオブジェクトを iterable にしても「だからなんやねん?」って感じなので、次は木を iterable にして見ます。

◯ 目標

2分探索木, Binary Search Tree を iterable にして見ましょう。

2分探索木とは、「「左の子孫の値 ≤ 親の値 ≤ 右の子孫の値」という制約を持つ二分木である。探索木のうちで最も基本的な木構造である。Wkipedia」だ、そうです。


f:id:domodomodomo:20171119172218p:plain


こんな操作がしたい。

>>> # 上図と同じ構造の木を作る。
>>> binary_search_tree = BinarySearchTree()
>>> binary_search_tree.insert_list([8, 3, 1, 6, 10, 4, 7, 14, 13])
>>> 
>>> # 木から1つ1つ要素を取り出す。
>>> for element in binary_search_tree:
        print(element)
1
3
4
6
7
8
10
13
14



まだ iterable でない2分探索木。ワイのキチガイコードを見ると面食らいますが、やってることは結構単純です。

insert メソッドは、木に要素を追加します。要素と等しいか、要素よりも小さければ左の木に追加, 要素よりも大きければ右の木に追加しているだけです。

class BinarySearchTree(object):
    def __init__(self):
        self.element = None
        self.left_tree = None
        self.right_tree = None

    def insert(self, new_element):
        if not self.element:
            self.element = new_element
        # 小さければ左の木に追加
        elif new_element <= self.element:
            if not self.left_tree:
                self.left_tree = BinarySearchTree()
            self.left_tree.insert(new_element)
        # 大きければ右の木に追加
        elif new_element > self.element:
            if not self.right_tree:
                self.right_tree = BinarySearchTree()
            self.right_tree.insert(new_element)

    def insert_list(self, lst):
        for new_element in lst:
            self.insert(new_element)

◯ 方針

次の2つのメソッドを実装する。

1. __iter__ メソッド

イテレータを返すメソッド。イテレータクラスは自作せず、 リストのイテレータを再利用しましょう。

class BinarySearchTree(object):
    ...
    def __iter__(self):
        # 2分探索木からリストを生成して、
        lst = self.generate_sorted_list()
        # そのリストのイテレータを返すようにします。
        return iter(lst)
2. generate_sorted_list メソッド

2分探索木からリストを生成するメソッド。このメソッドは、"左下にある要素から順に1つ1つ取っていきます"。

二分木は「左の子孫の値 ≤ 親の値 ≤ 右の子孫の値」という制約を持つので、左下の値が一番小さい値になります。なので "左下にあるものから順に要素を取って"いく と、自動的に小さい順にソートされた要素のリストが取れることになります。

木の全てのノードを辿る方法として、深さ優先探索と幅優先探索があります。今回は、深さ優先探索を利用して、"左下にあるものから順に要素を取っていきま" した。

class BinarySearchTree(object):
    ...
    def generate_sorted_list(self):
        """Get all data by depth-first search."""
        sorted_list = []
        if self.left_tree:
            sorted_list.extend(self.left_tree.generate_sorted_list())
        sorted_list.append(self.element)
        if self.right_tree:
            sorted_list.extend(self.right_tree.generate_sorted_list())
        return sorted_list

◯ 実装

generate_sorted_list と __iter__ を追加して iterable にした2分探索木。

class BinarySearchTree(object):
    def __init__(self):
        self.element = None
        self.left_tree = None
        self.right_tree = None

    def insert(self, new_element):
        if not self.element:
            self.element = new_element
        # 小さければ左の木に追加
        elif new_element <= self.element:
            if not self.left_tree:
                self.left_tree = BinarySearchTree()
            self.left_tree.insert(new_element)
        # 大きければ右の木に追加
        elif new_element > self.element:
            if not self.right_tree:
                self.right_tree = BinarySearchTree()
            self.right_tree.insert(new_element)

    def insert_list(self, lst):
        for new_element in lst:
            self.insert(new_element)

    def generate_sorted_list(self):
        """Get all data by depth-first search."""
        sorted_list = []
        if self.left_tree:
            sorted_list.extend(self.left_tree.generate_sorted_list())
        sorted_list.append(self.element)
        if self.right_tree:
            sorted_list.extend(self.right_tree.generate_sorted_list())
        return sorted_list

    def __iter__(self):
        # 2分探索木からリストを生成して、
        lst = self.generate_sorted_list()
        # そのリストのイテレータを返すようにします。
        return iter(lst)
        # もし要素を2倍する処理を追加したいなら
        # return iter(map(lambda e: e**2,self.generate_sortedlist()))


if __name__ == "__main__":
    # for 文で使える。
    binary_search_tree = BinarySearchTree()
    binary_search_tree.insert_list([8, 3, 1, 6, 10, 4, 7, 14, 13])
    for element in binary_search_tree:
        print(element)

    # iterable を引数に取る関数も使える。例 set 関数。
    tree_a = BinarySearchTree()
    tree_b = BinarySearchTree()
    tree_a.insert_list([1, 2, 3, 4, 5, 6])
    tree_b.insert_list([3, 4, 5, 6, 2, 1])
    print(set(tree_a) == set(tree_b))

ポイントは __iter__ メソッドが list の iterator をそのまま返しています。要素を取り出すだけなら iterator クラスの実装はせずに済みます。

もし取り出した要素に処理を行いたい場合は iterator クラスを実装することになります。それでも2倍にするなど簡単な操作だけの場合は map 関数を使えば iterator を実装せずに済みます。


9. iterator を自作する4 2分探索木(ジェネレータ)

ジェネレータを使うことにより、より簡潔にイテレータを表現できます。実際に先ほど自作した2分探索木に応用して、どれくらい簡潔に表現できるかを確認します。

◯ ジェネレータとは

ジェネレータも集合みたいなものです。この集合に要素を追加したい時は yiled 文を使います。

>>> def create_generator():
...   yield 1  # 要素を追加
...   yield 2  # 要素を追加
...   yield 3  # 要素を追加
...   yield 4  # 要素を追加
...   # raise StopIteration
...   #   末端に到達するか return が実行されると自動的に送出される。
... 
>>> 
>>> generator = create_generator()
>>>
>>> # yiled が使われた関数は generator クラスのオブジェクトを返す。
>>> type(generator)
<class 'generator'>
>>>
>>> # ジェネレータの要素を表示
>>> for element in generator:
1
2
3
4
>>>

◯ 2分探索木への応用

list を使った場合に比べて、短く書けています。再帰でジェネレータを使う場合は yield from を使用します。

class BinarySearchTree(object):

    # 短い
    def __iter__(self):
        if self.left_tree:
            yield from iter(self.left_tree)
        yield self.element
        if self.right_tree:
            yield from iter(self.right_tree)

    """
    # 長い
    def __iter__(self):
        lst = self.generate_sorted_list()
        return iter(lst)

    def generate_sorted_list(self):
        sorted_list = []
        if self.left_tree:
            sorted_list.extend(self.left_tree.generate_sorted_list())
        sorted_list.append(self.element)
        if self.right_tree:
            sorted_list.extend(self.right_tree.generate_sorted_list())
        return sorted_list
    """

全体のコードは、こちら
6_0_binary_serach_tree.py

◯ ジェネレータのもう1つのメリット

メモリの最大使用量を小さくできます。

例えば list に要素を格納して iter 関数で iterator を生成する場合は、for 文を実行する前に要素を list に全て格納するためのメモリ領域を確保しなければなりません。

反対に yield 文を使う場合は for 文で yield 文が呼び出されるたびに、次の要素を取りに行きます。使用するメモリの総量自体は変りませんが、メモリの瞬間最大使用量は下がります。

この2分木の例だと、メモリが.. と言われてもピンと来ませんが、例えばファイルを読み込むような処理をする場合は、ファイル全体を読み込んでしまうとメモリの使用料が一気に上がってしまいます。yiled を使い1行読み込み捨てて、また1行見込み1行捨てるようにすれば、そのような事態は避けられます。

◯ ジェネレータのデメリット

リストに比べて、for 文の実行が 2, 3 割ほど実行速度が遅くなります。実際の比較はリンク先で行なっています。
3. イテレータとジェネレータの for 文の速度の比較

しかし、何事にもさじ加減や状況によるものはあると思いますが、たとえ 2, 3 割実行速度が遅くなったとしてもコード数を簡潔に半分以下に書けるなら、半分以下に書ける方を取るのが望ましいと思います。

Python で書くなら、実行速度よりも可読性を優先するべきです。もし、この塩梅で可読性を犠牲にしてまでも実行速度を求めるようとしたなら、それは違う言語の使用を検討する時です。

◯ 用語の整理

ジェネレータという言葉には2つの意味があるので、留意しておいてください。

generator(ジェネレータ)
通常はジェネレータ関数を指しますが、文脈によっては ジェネレータイテレータを指す場合があります。 意図された意味が明らかでない場合、 明瞭化のために完全な単語を使用します。

ジェネレータ関数 (generator function)
yield 文 (yield 文 の節を参照) を使う関数もしくはメソッドを ジェネレータ関数 と呼びます。そのような関数が呼び出されたときは常に、関数の本体を実行するのに使えるイテレータオブジェクトを返します: イテレータiterator.__next__() メソッドを呼び出すと、 yield 文を使って値が提供されるまで関数を実行します。関数の return 文を実行するか終端に達したときは、 StopIteration 例外が送出され、イテレータが返すべき値の最後まで到達しています。

generator iterator(ジェネレータイテレータ)
generator 関数で生成されるオブジェクトです。
yield のたびに局所実行状態 (局所変数や未処理の try 文などを含む) を記憶して、処理は一時的に中断されます。 ジェネレータイテレータ が再開されると、中断した箇所を取得します (通常の関数が実行の度に新たな状態から開始するのと対照的です) 。

10. どういう時にイテレータを使うべき?どういう時に使ったらいけないの?

答え: コードの可読性があがるとき

Python の機能を使って抽象化、簡潔化することが言外にあるのかな、と。感じたりします。抽象化し過ぎてしまうとコードの詳細な動作がわからなくなるというリスクを取りつつも、Python の機能を積極的に用いて簡潔に記述して可読性の向上を図るのが、1つの指針なのかもしれない。

この理由は、第1にPEP 8 では 1 行の文字数を 79 文字に限定しているから。長くても 99 文字と短い。第2に Python が抽象化を支援する機能を提供しているから。第3に仮に /( ) を使うにしても、改行すると可読性は下がるから。

これら3つを組み合わせると適切に抽象化できるならば、抽象化して1行の文字数を減らすことの方が望ましいと言うことが推論できる。
Python PEP 8 の最大 79 文字について

イテレータを使うメリット: 記述がシンプルになります。

iterator を使って、ほんの少しですが記述を簡潔にすることができます。例えば、一番最初の例で見た通り member_list のような属性を参照することなく、直接 for 文や itereable なオブジェクトを引数に取る関数に代入しています。特に木など集合を表現するクラスに応用しやすいと感じています。

# iterable にする
for element in binary_search_tree:
    pass
# iterable にしない, list を使う
for element in binary_search_tree.generate_sorted_list():
    pass

イテレータを使うデメリット: 動作の詳細がわからない。

このコードでは、順序で要素を出力してくれるかわかりません。

# iterable にする
for element in binary_search_tree:
    pass



過度な抽象化は逆にコードの可読性を下げるそうです。

コードが理解しづらい
 4. コメントなしに低レベルの最適化が施されている
 5. コードが賢すぎる
コードが追いかけづらい
 4. 全てが抽象化されすぎている
まずコードの可読性を最適化しよう | プログラミング | POSTD



ちょっと本題からはづれてしまいますが、Python2 では reduce 関数が組み込み関数として使えました。 Python3 では reduce 関数は、組み込み関数から除外されて functools から import することになりました。過度な抽象化を避けると言う意味ではひとつの例かなと思ったりもします。

# 
import functools
lst = [1, 2, 3, 4, 5]

# 1. 明示的に累積ループで書く
def product(s):
    p = 1
    for e in s:
        p = p * e
    return p
product(lst)  # 120

# 2. reduce で書く
functools.reduce(lambda x,y: x*y, lst)  # 120

# 3. Python2 では import しなくても書けた
# reduce(lambda x,y: x*y, lst)  # 120



なぜかと言うと読み辛いからだそうです。以下のブログは Guido のブログからの引用です。

今度は reduce 関数について考えよう。これは実際私がいつも最も憎むものだ。+ もしくは * を含む2、3の例を除けば reduce 関数はいつもパッと見ではわからないような関数を引数にとって呼び出されている。私は reduce 関数が何をしているかを理解する前に、引数として与えられた関数に、実際に何が与えられているのかを図示するために紙とペンを取らないといけない。したがって私の中では reduce 関数が適用できるのは、結合演算子にごく限定される(訳注: 結合演算子 ... 例えば +, -, *, / などの四則演算子)。それ以外の事例では明示的に累積ループを書いた方がよい。
So now reduce(). This is actually the one I've always hated most, because, apart from a few examples involving + or *, almost every time I see a reduce() call with a non-trivial function argument, I need to grab pen and paper to diagram what's actually being fed into that function before I understand what the reduce() is supposed to do. So in my mind, the applicability of reduce() is pretty much limited to associative operators, and in all other cases it's better to write out the accumulation loop explicitly.
The fate of reduce() in Python 3000 by Guido van van Rossum

Python と Go 言語

Python は iterable にすることによって、書き方をシンプルにできる。ただ iterable にしない書きかもできて、その分だけ書き方に幅ができてしまいます。

Python って誰が書いても同じようなコードになりますみたいな言葉を誰かから聞いた気がして、個人的にもそう言うのが好きなので、ちょっと気持ち悪い気もします。

反対に Go 言語というものがあります。Go 言語は、機能を削って設計された言語のようです。その分だけ記述には、どうしても煩雑さを要してしまうところもあります。ただ、書き方は、より統一されるみたいな感じになるようです。

ここで機能を削ってできたと言われる Go 言語ならどんな扱いになってるんやろうか..と気になったのですが、まだよくわかりませんが..、なんとなく Pythonイテレータと同等の機能を提供するものは無さそうです。

それならサービスが固まってない段階は Python が提供する様々な機能を積極的に使ってシンプルに書いて、そこからサービスが固まったら Go でリファクタリングして処理速度を求めて低レベルな記述に書き写して書くというのは、ある意味正しい流れなのかもしれない..。Rust の位置付けはどうなのだろうか...。
なぜ私達は Python から Go に移行したのか - Frasco



11. 内部イテレータと外部イテレータ

内部イテレータ、外部イテレータは、どのプログラミング言語でも共通して使われる単語です。簡単に触れておきたいと思います。

◯ 外部イテレータ

f:id:domodomodomo:20171127163607j:plain
iterable なオブジェクトと iterator を切り分けて別々のクラスで設計されている場合、その iterator外部イテレータと呼ばれます。上でせっせと、自作していたのは、全て外部イテレータです。

◯ 内部イテレータ

f:id:domodomodomo:20171127163715j:plain
反対に iterable なオブジェクトと iterator が同じクラスで設計されている場合、その iterator内部イテレータと呼ばれます。

Python では内部イテレータも実装できます。container と iterator を分けて作らなくても、実装することができます。これは簡単で container に __iter__ と __next__ を実装してしまえばことは済みます。

◯ 外部イテレータを持つ組み込み型の例
list, dic, str 型

普段よく使う list, dictionary, string などの組み込み型の iterator の処理は、それぞれ専用のクラス list_iterator, dict_keyiterator, str_iterator クラスが用意され、container とは別に実装されています。

最初からユーザが定義せずとも使用できる list, dic, str などの型を組み込み型と呼びます。

>>> iter([1, 2, 3])
<list_iterator object at 0x1053d52b0>
>>> iter({'a':1, 'b':2, 'c':3})
<dict_keyiterator object at 0x1053c3a98>
>>> iter('Yaruo')
<str_iterator object at 0x1053d5208>

◯ 内部イテレータを持つ組み込み型の例
list_iterator, dict_keyiterator, str_iterator

iterator 自身も iterator と container の実装を分けていない iterable な container オブジェクトと見做すことができます。下記のコードで for 文の in の中に直接 iterator オブジェクトである list_iter を記述できています。

>>> list_iter = iter([1, 2, 3])
>>> 
>>> for e in list_iter: e
... 
1
2
3
>>> # for 文から抜けると空っぽに。
>>> list(list_iter)
[]


このように通常は内部イテレータの場合は、for 文を回すと空っぽになります。なので、もし使い終わった iterator を元に戻すと言う操作をしたい場合は、専用の iterator クラスを設けて外部イテレータとして設計し直す必要があります。

◯ 内部イテレータを持つ組み込み型の例 その2
generator 型

ジェネレータは、内部イテレータです。

>>> def create_generator():
...   yield 1  # 要素を追加
...   yield 2  # 要素を追加
...   yield 3  # 要素を追加
...   yield 4  # 要素を追加
... 
>>> 
>>> generator = create_generator()
>>>
>>> # 内部イテレータの特徴1
>>> #   iter は自分自身を返す。
>>> generator is iter(generator)
True
>>> 
>>> # generator は集合を表現, list 形式で要素を出力した。
>>> list(generator)
[1, 2, 3, 4]
>>> 
>>> # 内部イテレータの特徴2
>>> #   for 文を使うとイテレータは空っぽに
>>> #   list(generator) == [i for i in generator] この2つは等価
>>> list(generator)
[]
>>> 

疑問: なぜ container 自体も集合を取り扱うクラスなのに、わざわざ集合を取り扱うクラスを iterator として別に定義しているのでしょうか?(なぜわざわざ外部イテレータを設けるのでしょうか?)

答え: container と iterator が所持するデータを分けることができるから(個人の感想)。

内部イテレータを用いると for 文で回すと空っぽになります。例えば上の "3. 内部イテレータを持つ組み込み型" で内部イテレータを自身 list_iterator を for 文に渡したら、空っぽになりました。外部イテレータを実装すれば、そのような事態も避けることができます。

外部イテレータの欠点は、1つ目は、実装が面倒であること。わざわざ iterator クラスを専用に実装しないといけません。2つ目は、iter 関数でイテレータを生成するのは、実質的にコピーを取るような処理が必要になること。その分、コピーにかかる処理の時間と、取ったコピーを確保するメモリの消費量が増えます。

外部イテレータを使わず、無理やり内部イテレータで対処することもできるとは思います。例えば、空っぽになるような事態を避けるために、1つのオブジェクトに2つのデータが存在する様に実装すればいいのです、が...

そのように実装してしまうと、1つのオブジェクトの中で同じ意味合いを持つ2つのデータの状態が異なるような事態が発生するような状態が発生してしまいます。

1つのオブジェクトの中で、一方のイテレータ用のデータが for 文を回すと空っぽになり、もう片方のデータは変化が無いような状態です。それは、なんとなく気持ち悪い感じがします。

iter 関数は container のコピーを返すわけではない。

これまで見てきた通り、内部イテレータを利用する場合、iter 関数は container 自身を返します。一番最初に iter 関数は、container の集合をコピーしますと言うニュアンスで伝えましたが、コピーを返すわけではありません。

しかも Python でコピーと言われると shallow copydeep copy の操作を指しているのかと思われたかもしれません(8.10. copy — 浅いコピーおよび深いコピー操作)iterator は、コピーでさえなく、このあたりの言葉の使い方も、誤解や不信を与えたかと思います。

さらに言えば iterator は集合を表現している必要もなく、要素を出力する必要もありません。指定されたメソッドさえ実装されていれば iterable なのです。

ちなみに、もともと iterator の iterate とは "取り出す" ことを繰り返すと言うよりも、"同じ処理" を繰り返す、と言う意味合いで iterate と言う言葉が使われている気配があります。

そこで、このページの最後では Python における iterable とは何かをコードで表現したいと思います。


12. イテレータを元に戻したい、リセットしたい。

特に内部イテレータを実装した場合、イテレータを for 文で回すと空っぽになることを知らないと、イテレータを元に戻したいけど、どうすればいいんだろう... ということに陥ります。イテレータを元に戻したいなと思った時は、外部イテレータを実装するときかなと思ったりもします。

◯ 外部イテレータの場合

外部イテレータの場合は、for 文で回しても空にならないので、そう困ることはありません。

>>> list_ = [0, 1, 2, 3]
>>> outer_iterator = iter(list_)
>>> 
>>> # iterator を使って
... next(outer_iterator)
0
>>> next(outer_iterator)
1
>>> next(outer_iterator)
2
>>> next(outer_iterator)
3
>>> 
>>> # 空っぽになっても
>>> list(outer_iterator)
[]
>>> 
>>> # iter 関数を呼べば
>>> outer_iterator = iter(list_)
>>>
>>> # 元に戻る。
>>> list(outer_iterator)
[0, 1, 2, 3]

◯ 内部イテレータの場合

方法1 もう一度イテレータを呼び出す。

もしメモリの使用料が気になるなら、再度イテレータを呼ばした方が、望ましいでしょう。

方法2 iterator クラスと container クラスを分割する。

いままで見てきた通り、iterator クラスと container クラスを分割して、内部イテレータから外部イテレータに再設計します。そうすれば for 文から抜けた後も、イテレータが空っぽになったりするようなこともありません。

方法3 list にデータを保存する。
>>> # generator は内部イテレータ
>>> def create_generator():
...     yield 0
...     yield 1
...     yield 2
...     yield 3
... 
>>> 
>>> inner_iterator = iter(create_generator())
>>> 
>>> # inner_iterator を container に保存
... container = list(inner_iterator)
>>>
>>> # iterator を使う。
>>> iterator = iter(container)
>>> 
>>> next(iterator)
0
>>> next(iterator)
1
>>> next(iterator)
2
>>> next(iterator)
3
>>> 
>>> # 空っぽになっても
>>> list(iterator)
[]
>>>
>>> # iter 関数を呼べば
>>> iterator = iter(container)
>>>
>>> # 元に戻る。
>>> print(list(iterator))
[0, 1, 2, 3]

13. 用語

復習を兼ねて、上記の動作例で使用した組み込み関数、オブジェクトメソッド、発生した例外を一度、整理して見たいと思います。

以下は、マニュアルからの抜粋になります。

container
他のオブジェクトに対する参照をもつオブジェクトもあります; これらは コンテナ (container) と呼ばれます。コンテナオブジェクトの例として、タプル、リスト、および辞書が挙げられます。オブジェクトへの参照自体がコンテナの値の一部です。
— ワイの注記 container について記述されている箇所の抜粋しました。タプル、リスト、および辞書など集合を表現するオブジェクトを container だと言いたい様子。ただ、この定義だと全てのオブジェクトが container に該当してしまうんじゃまいか..

iter(object[, sentinel])
イテレータ (iterator) オブジェクトを返します。 第二引数があるかどうかで、第一引数の解釈は大きく異なります。

container.__iter__()
イテレータオブジェクトを返します。

iterator.__iter__()
イテレータオブジェクト自体を返します。

next(iterator[, default])
iterator の __next__() メソッドを呼び出すことにより、次の要素を取得します。イテレータが尽きている場合、 default が与えられていればそれが返され、そうでなければ StopIteration が送出されます。

iterator.__next__()
コンテナの次のアイテムを返します。

StopIteration
組込み関数 next() と iterator の __next__() メソッドによって、そのイテレータが生成するアイテムがこれ以上ないことを伝えるために送出されます。

generator(ジェネレータ)
generator iterator を返す関数です。 通常の関数に似ていますが、 yield 式を持つ点で異なります。 yield 式は、 for ループで使用できたり、next() 関数で値を 1 つずつ取り出したりできる、値の並びを生成するのに使用されます。
通常はジェネレータ関数を指しますが、文脈によっては ジェネレータイテレータを指す場合があります。 意図された意味が明らかでない場合、 明瞭化のために完全な単語を使用します。

ジェネレータ関数 (generator function)
yield 文 (yield 文 の節を参照) を使う関数もしくはメソッドを ジェネレータ関数 と呼びます。そのような関数が呼び出されたときは常に、関数の本体を実行するのに使えるイテレータオブジェクトを返します: イテレータiterator.__next__() メソッドを呼び出すと、 yield 文を使って値が提供されるまで関数を実行します。関数の return 文を実行するか終端に達したときは、 StopIteration 例外が送出され、イテレータが返すべき値の最後まで到達しています。

generator iterator(ジェネレータイテレータ)
generator 関数で生成されるオブジェクトです。
yield のたびに局所実行状態 (局所変数や未処理の try 文などを含む) を記憶して、処理は一時的に中断されます。 ジェネレータイテレータ が再開されると、中断した箇所を取得します (通常の関数が実行の度に新たな状態から開始するのと対照的です) 。

14. イテレータとは

復習を兼ねて、言葉から一般論を追いかけてみます。

◯ 辞書では

辞書では、どのように説明されているのでしょうか?

iterate
 (Vi) 繰り返し適用される
 (Vt) ~を繰り返して言う, ~を反復する
~tor
 ~する人
英辞郎 on the WEB:アルク

「繰り返すもの」ってことですかね?

Wikipedia では

イテレータ(英語: iterator)とは、プログラミング言語において配列やそれに類似する集合的データ構造(コレクションあるいはコンテナ)の各要素に対する繰り返し処理の抽象化である。
イテレータ - Wikipedia

いまいちよくわかりません??

Python の公式ドキュメントでは

Python では、どのように説明されているのでしょうか?

iterator
 データの流れを表現するオブジェクトです。
用語集 — Python 3.6.3 ドキュメント









f:id:domodomodomo:20171106122644j:plain


もっとよくわからなくなりました???笑









ここから下はもう少し

 「iterable って、正確にはなんだろう?」
 「container って、集合じゃなくても良くない?」

って疑問に思った方だけ読んでいただければと思います。



理解するときは container は集合で iterator はそのコピーみたいなものと考えると理解しやすいです。

しかし、実際の実装では別に container や iteraotr が集合である必要も __next__ メソッドがその要素を取り出す必要もありません。どう言うことかと言えば、マニュアルで指定された __next__ メソッドや __iter__ メソッドが定義さえされていれば for 文で問題なく使うことができます。

だから Python の公式ドキュメントは、イテレータを「データの流れを表現するオブジェクトです。」というなんともわからない表現をしています。例えば、公式チュートリアルのサンプルコードにある Reverse クラスは、iterable ですが集合を表現している訳でもないですし、__next__ メソッドは要素を返している訳でもありません。
9.8. イテレータ < 9. クラス < Python チュートリアル

ここから先では文章ではなく、コードから、iterable であるかどうかを判定するコードから、イテレータの理解を深めていきたいと思います。



15 章...iterable の定義
16 章...① isiterable 関数
17 章...② isiterable 関数(詳細版)
18 章...③ assertIsIterableContainer 関数

15. iterable の定義

for 文の in にいれることができれば iterable だと言えそうです。

iterable
構成要素を一度に一つずつ返すことができるオブジェクトです。構成要素を一度に一つずつ返すことができるオブジェクトです。

イテラブルの例には、(list 、 str 、 tuple のような) 全てのシーケンス型 、 dict ... などがあります。

イテラブルは for ループやその他シーケンスが必要な多くの場所 (zip() 、 map() 、 ...) で使えます。

用語集 — Python 3.6.3 ドキュメント

What exactly are Python's iterator, iterable, and iteration protocols?


16. ① isiterable 関数

簡単に言えば for ~ in ... の ... に入れても動作できるかどうかを判定します。

def isiterable(container):
    return hasattr(container, '__iter__') 



これで十分です。あれだけ盛り上げておいてこれかよ、って感じですが..。

◯ でも、別の用途で __iter__ メソッドが定義されているかもしれないけどいいの?

答え: 問題ありません。

Python では __iter__ は全てのオブジェクトが iterator を返す様にマニュアルで定められているからです。

前後左右に2つのアンダースコアで挟まれた変数、または属性は __*__ 、マニュアルで記載された以外の用途で使ってはならないことになっています。

例えば __init__ メソッドを初期化以外の別の用途で使ったら、大変なことになってしまいますよね。

このドキュメントで明記されている用法に従わない、 あらゆる __*__ の名前は、いかなる文脈における利用でも、警告無く損害を引き起こすことがあります。
2. 字句解析 — Python 3.6.3 ドキュメント

◯ duck typing (Java を知らない人は読み飛ばしてください。)

__iter__ メソッドさえ定義されていれば iterable だと言えます。

Java とは違い Python では、例えば iterable という interface が継承されていなくても、同じ名前の method さえ定義されていれば、iterable という interface を実装している様に振舞うことができます。この様な型付の性質は duck typing と呼ばれたりします。

iterable という interface が宣言されていなくても(duck というinterface がされていなくとも)、iter メソッドを実装していて、そいつが iterator のように振る舞うなら(duck のように鳴き、よちよち歩くなら)、そいつは iterator だ!(duck だ!)という意味合いだそうです。

static typing が静的型付け, dynamic typing が動的型付けと訳されるなら、duck typing は鴨的型付けって訳になるんですかね..。
デザインパターン「Iterator」-Qiita (Java での iterator の例)

また mypy を使う場合は duck typing を許してくれなさそうな気配があります。例えば mypy を使うと __iter__ を実装しているだけでは iterable とみなしてくれず、ちゃんとクラス定義時に iterable であることを明示するオブジェクト Iterable を継承しないとエラーを返されます。

from typing import Iterator, Iterable

# NG -> error: Iterable expected
# class Foo(object):
class Foo(Iterable):
    def __iter__(self) -> Iterator[int]:
        yield 1
        yield 2

for x in Foo():
    print(x)

Mypy doesn't recognize objects implementing __iter__ as being Iterable · Issue #2598 · python/mypy · GitHub

17. ② isiterable 関数(詳細版)

iterable container, iterator が __iter__, __next__ メソッドを実装しているかどうかの判定しています。

def isiterable(container):
    # container
    if not hasmethod(container, '__iter__'):
        return False
    # iterator
    iterator = container.__iter__()
    if not hasmethod(iterator, '__iter__'):
        return False
    if iterator.__iter__() is not iterator:
        return False
    if not hasmethod(iterator, '__next__'):
        return False
    return True


def hasmethod(obj, method_name):
    return hasattr(obj, method_name) \
        and callable(getattr(obj, method_name))


要素を全て出力したら StopIteration を吐くかどうかの判定は行いません。

iterator を生成して、StopIteration を吐くかどうかを確認するためには、要素数の数だけ next 関数を呼び出すしかありません。この実装は次の2つの理由から却下しました。

第1に iterator オブジェクトが副作用を持っている可能性があるから。第2に要素数の数だけ next 関数を呼び出すのは時間がかかるから。


18. ③ assertIsIterableContainer 関数

isiterable 関数より詳しく StopIteration を正しく吐くかどうかまで判定するコードです。 あるいは、イテレータプロトコルが正しく実装されているか判定するコードです。Python ではオブジェクト間の定められたやり取りをプロトコルと表現しているようです。

ここまで来ると isiterable と言う判定よりも、test に近くなるので assert を吐くようにしました。使い所はびっくりするほど全くないと思いますが、unittest の練習がてらに書いてみました。適当に流しで眺めていていただけると嬉しいです。

◯ 使い方

sample.py

import unittest 
class TestIterator(unittest.TestCase):
    def test_iterator(self):
        iterable_container = IterableContainer(
            'Trump', 'Obama', 'Clinton')
        # success
        assertIsIterableContainer(iterable_container, 3)
        # error
        assertIsIterableContainer(iterable_container, 4)





unittest を実行するも num_of_elements 4 より小さい繰り返し回数 3 回で StopIteration が raise されたのでエラーで返されている。

$ python -m unittest sample.TestIterator
F
======================================================================
FAIL: test_iterator (iterator4.TestIterator)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "sample.py", line 108, in assertIsIterableContainer
    itr.__next__()
  File "sample.py", line 41, in __next__
    raise StopIteration('No name is stocked...')
StopIteration: No name is stocked...

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "sample.py", line 72, in test_iterator
    assertIsIterableContainer(iterable_container, 4)
  File "sample.py", line 118, in assertIsIterableContainer
    'but less than num_of_elements.')
AssertionError: StopIteration arised, but less than num_of_elements.

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)


◯ assertIsIterableContainer 関数

def assertIsIterableContainer(container, num_of_elements):
    if not(isinstance(num_of_elements, int) and num_of_elements >= 0):
        raise ValueError('num_of_element sholud be zero or a natural number.')

    # 1) container.__iter__()
    assert hasmethod(container, '__iter__'), \
        'container does not have __iter__ method.'
    iterator = container.__iter__()

    # 2) iterator.__iter__()
    # Does this function return self?
    assert hasmethod(iterator, '__iter__'), \
        'iterator does not have __iter__ method.'
    assert iterator is iterator.__iter__(), \
        'iteraotr.__iter()__ does not return himself.'

    # 3) iterator.__next__()
    assert hasmethod(iterator, '__next__'), \
        'iterator does not have __next__ method.'

    # 4)
    # After popping all elements,
    # does this function raise StopIteration?
    k = -1
    while True:
        try:
            k = k + 1
            next(iterator)

        except StopIteration:
            # 4-0) success
            if k == num_of_elements:
                break
            # 4-1) less than num_of_elements
            elif k < num_of_elements:
                raise AssertionError(
                    'StopIteration arised, ' +
                    'but less than num_of_elements.')

        else:
            # 4-2) more than num_of_elements
            if k == num_of_elements:
                raise AssertionError(
                    'iterator\'s method __next__ called, ' +
                    'but more than num_of_elements.')
    return


def hasmethod(obj, method_name):
    return hasattr(obj, method_name) \
        and callable(getattr(obj, method_name))


class Container(object):
    def __init__(self, *args):
        # type(args) is tuple
        self.tuple = args

    # container.__iter__()
    def __iter__(self):
        return Iterator(*self.tuple)


class Iterator(object):
    def __init__(self, *args):
        self.list = list(args)

    # iterator.__iter__()
    def __iter__(self):
        return self

    # iterator.__next__()
    def __next__(self):
        if self.list:
            return "Hello, " + self.list.pop() + "."
        else:
            raise StopIteration('No name is stocked...')


import unittest
class TestIterator(unittest.TestCase):
    def test_iterator(self):
        iterable_container = IterableContainer(
            'Trump', 'Obama', 'Clinton')
        # success
        assertIsIterableContainer(iterable_container, 3)
        # error
        assertIsIterableContainer(iterable_container, 4)



19. おわりに

最後まで、お読みいただき、ありがとうございました。



みなさまへの大切なメッセージ