Python のイテレータ

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

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

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


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

例えば、このように list を生成して for 文を回していたのが

>>> for e in 自分で定義したクラスのオブジェクト.generate_list():
...     print(e)
今日も
ドッタン
バッタン
大騒ぎ
>>>



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

>>> for e in 自分で定義したクラスのオブジェクト:
...     print(e)
今日も
ドッタン
バッタン
大騒ぎ
>>>



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



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

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



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

>>> set(自分で定義したクラスのオブジェクトa.generate_list()) \
...     - set(自分で定義したクラスのオブジェクトb.generate_list()) 



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

>>> set(自分で定義したクラスのオブジェクトa) \
...     - set(自分で定義したクラスのオブジェクトb) 



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


◯ 期待される効果

可読性を上げることができます。

iterator を使って、記述を簡潔にすることができます。例えば上で見た通り generate_list のようなメソッドを呼び出すことなく、直接 for 文や itereable なオブジェクトを引数に取る関数に代入しています。特に木など集合を表現するクラスに応用しやすいと感じています。


◯ 実装の仕方

やり方は簡単で、次のような iterator を返す __iter__ メソッドを追加するだけでできるようになります。

class 自分が定義したクラスのオブジェクト:
    def __iter__(self):
        # iter 関数:
        #   list, set, dict などを引数にとり
        #   iterator を返す組み込み関数
        return iter(self.generate_list())


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
>>>

element = next(iterator) という処理が、文字通り繰り返されています(iterate されています)。

もし 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__ メソッドを実装さえしてしまえば良さそうですね。

6. iterator を自作する1

やっと自作するところまでたどり着きました。

◯ 目標

このクラスを

class Container:
     pass



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

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

◯ 方針

公式のマニュアルに従い実装を進めていきます。

Step1. container オブジェクト

まず iterate させたい値を持つ container オブジェクトに対して iterator オブジェクトを返す組み込み関数 __iter__() を定義する。

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

Step2. iterator オブジェクト

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

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

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

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

4.5. イテレータ型


イテレータオブジェクト自体を返します。 iterator.__iter__() と言うのが出てきたので、図を更新します。
f:id:domodomodomo:20171126131853j:plain

container と iterator の関係2

◯ 実装

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


class Iterator:
    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():
    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():
    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 len(self.list) == 0:
            raise StopIteration('No name is stocked...')
        return "Hello, " + self.list.pop() + "."

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



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

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

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

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

class BinarySearchTree():
    ...
    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():
    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. iterable にするメリットとデメリット
抽象化するメリットとデメリット

◯ メリット: 記述がシンプルになります。

# iterable にする
for element in binary_search_tree:
  pass

◯ デメリット: 動作の詳細がわからない。

リストのまま扱えば順番がわかります。その分だけ記述が長くなってしまいますが。

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

◯ どういう時に iterable にするべき?

答え: わかりません。順番が問われない集合を比較したりする場合には有効かもしれません。

set(tree_a) == set(tree_b)

Python と Go 言語

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

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

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

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

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

中途半端な抽象化は逆にコードの可読性を下げるそうです。

コードが理解しづらい
 4. コメントなしに低レベルの最適化が施されている
 5. コードが賢すぎる
コードが追いかけづらい
 4. 全てが抽象化されすぎている

まずコードの可読性を最適化しよう | プログラミング | POSTD

◯ どっちでもいいときは、どっちを取ってコーディングスタイルを整えるべきだろう?

抽象化して簡潔に。ただし、変数名は、適切な長さを用いて.. かな..

PEP 8 では 1 行の文字数を 79 文字に限定している。長くても 99 文字だ。そのことを踏まえると、コードの詳細な動作がわからなくなるリスクを取りつつも、Python の機能を積極的に用いて簡潔に記述して可読性の向上を図るのが、1つの指針なのかもしれない。

PEP 8 には 1 行を 79 文字に制限する理由も記されている。コードレビューを前提に、エディタに改行させないように自分で改行の仕方を指定しろって意味合いだろうけど..。仮に / で改行させるにしても、可読性は下がるし...。

ちなみにこの 79 という数字はパンチカードから来ているのでは、という話を聞いたことがあります。

特に明確に書かれているわけではないけど、ある程度 1 行の行数が短いということは、Python の機能を使って抽象化、簡潔化することが言外にあるのかな、と。

変数名も短くした方が.. というのもありますが、いくつかの資料を漁っていると変数名を短くすることは、あまり良いものとは取られていないような気がします..。ここはケチらない方が良さそう。

以下は、PEP 8 を抜粋したものの和訳です。

すべての行を最大 79 文字に制限する。
Limit all lines to a maximum of 79 characters.

コードの後に続く docstring やコメントのような構造的な制限の少ない長いブロックのテキストについては、1 行の長さは 72 字以内にするべきだ。
For flowing long blocks of text with fewer structural restrictions (docstrings or comments), the line length should be limited to 72 characters.

エディタのウィンドウを表示する際に必要な幅を制限できれば、複数のファイルを並べることが可能になり、2 つのバージョンのコードを隣接して左右に並べて、コードリビューツールを使うときに効果的である。
Limiting the required editor window width makes it possible to have several files open side-by-side, and works well when using code review tools that present the two versions in adjacent columns.

大抵のツールが提供する、デフォルトで長い 1 行を折り返して表示してくれる機能(wrapping)は、コードの見た目の構造を破壊し、より理解を困難なものにする。
The default wrapping in most tools disrupts the visual structure of the code, making it more difficult to understand.

この 1 行の文字数の制限は、1 行が 80 文字のエディタが折り返して表示する機能を避けるために選定されたものである。もし、たとえツールが目印として 1 行の文字を複数行で折り返し表示したときに、最後の文字に印を置いてくれるような機能がったとしても勝手に折り返されるのを避けるために1行を 79 文字に制限するべきである。
The limits are chosen to avoid wrapping in editors with the window width set to 80, even if the tool places a marker glyph in the final column when wrapping lines.

いくつかのウェブベースのツールは、自動的に行を折り返してくれるようなことは、全くしてくれないかもしれない。
Some web based tools may not offer dynamic line wrapping at all.

PEP 8 -- Style Guide for Python Code | Python.org

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

◯ 外部イテレータ

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

◯ 内部イテレータ

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

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

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

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

しかも Python でコピー, copy と言えば一般に shallow copy, copy.copy(x) の操作を指します。iterator は、コピーでさえなく、このあたりの言葉の使い方も、誤解や不信を与えたかと思います。

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

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

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

◯ 外部イテレータを持つ組み込み型
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 クラスを設けて外部イテレータとして設計し直す必要があります。

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

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

yiled 文を使うと関数は generator オブジェクトを返します。generator オブジェクトは、集合を表現しています。generator オブジェクト, 集合に要素を追加したい時は、yiled を使います。

>>> def create_generator():
...   yield 1  # 要素を追加
...   yield 2  # 要素を追加
...   yield 3  # 要素を追加
...   yield 4  # 要素を追加
... 
>>> 
>>> generator = create_generator()
>>>
>>> # yiled が使われた関数は generator を返す。
>>> type(generator)
<class '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 文に渡したら、空っぽになりました。もちろん、空っぽになっても構わないなら、内部イテレータの方が望ましいでしょう。

外部イテレータを実装すれば、そのような事態も避けることができます。欠点は、実装が面倒。わざわざ iterator クラスを専用に実装しないといけないということでしょうか。

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

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

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


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

◯ 外部イテレータの場合

>>> 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 iterator クラスと container クラスを分割する。

いままで見てきた通り、iterator クラスと container クラスを分割して、内部イテレータから外部イテレータに再設計します。

方法2 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]

12. 用語

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

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

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__() メソッドによって、そのイテレータが生成するアイテムがこれ以上ないことを伝えるために送出されます。

13. イテレータとは

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

◯ 辞書では

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

iterate
 (Vi) 繰り返し適用される
 (Vt) ~を繰り返して言う, ~を反復する

英辞郎 on the WEB:アルク

~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 文で問題なく使うことができます。

ここから先では文章よりもコードから iterable であるかどうかを判定して、理解を深めていきたいと思います。

14. 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?


15. 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 であることを明示するオブジェクトを継承しないとエラーを返されます。

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

16. 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 関数を呼び出すのは時間がかかるから。


17. assertIsIterableContainer 関数

isiterable 関数より詳しく StopIteration を正しく吐くかどうかまで判定するコードです。

ここまで来ると 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():
    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):
        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)

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

答え: 自作の 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():
    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():
    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

19. おわりに

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

Remove all ads