Subscribed unsubscribe Subscribe Subscribe

Python の iterator

Django

iterator とは

iterator
イテレーター, 反復子
複数の要素の集まりと見なせる対象(配列・リストなど)で
その各要素に順次アクセスするための変数や言語機能。

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

-tor
~ する人

Python における iterator とは

iterable なオブジェクトからオブジェクトメソッド __iter__() を呼んで帰ってきたオブジェクトが iterator になります。

ちなみに iterable なオブジェクトとは for ~ in ... の ... に当たるところに入ることができるオブジェクトです。

例えば list, dict, str クラスが iterable なオブジェクトです。


具体的には...

iterator オブジェクトの呼び出し

>>> # list クラスは iterable なオブジェクト
>>> container = [1, 2, 3, 4]
>>> type(container)
<class 'list'>
>>> 
>>> # __iter__ メソッド
>>> # iterator を返します。
>>> iterator = container.__iter__()
>>> type(iterator)
<class 'list_iterator'>

iterator オブジェクトが持つ2つのメソッド

>>> # 1) __iter__ メソッド
>>> # iterator 自分自身を返します。
>>> iterator is iterator.__iter__()
True 
>>>
>>>
>>> # 2) __next__ メソッド
>>> # 次の要素を返します。
>>> iterator.__next__()
1
>>> iterator.__next__()
2
>>> iterator.__next__()
3
>>> iterator.__next__()
4
>>> iterator.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

具体的には...(組み込み関数からの呼び出し)

iterator オブジェクトの呼び出し

>>> # list クラスは iterable なオブジェクト
>>> container = [1, 2, 3, 4]
>>> type(container)
<class 'list'>
>>> 
>>> # __iter__ メソッド
>>> # iterator を返します。
>>> iterator = iter(container)
>>> type(iterator)
<class 'list_iterator'>

iterator オブジェクトが持つ2つのメソッド

>>> # これが iterator
>>> iterator = iter(container)
>>>
>>> # iterator は iterator 自身を返す
>>> iterator is iter(iterator)
True 
>>>
>>>
>>> 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

マニュアル

上記の動作例で使用した組み込み関数、オブジェクトメソッド、発生したエラーをマニュアルから抜粋しました。

container.__iter__()

イテレータオブジェクトを返します。

iterator.__iter__()

イテレータオブジェクト自体を返します。

iter(object[, sentinel])

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

iterator.__next__()

コンテナの次のアイテムを返します。

next(iterator[, default])

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

StopIteration

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



自作もできる

マニュアルの文言を見ると iterator を自作できそうですね。

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

4.5. イテレータ型

目標

では自作の IterableContainer クラスを iterable にして for 文で name1, name2, name3 を取得できるようにしましょう。

class IterableContainer():
  def __init__(self, name1, name2, name3):
    self.name1 = name1
    self.name2 = name2
    self.name3 = name3

実行例

>>> iterable_container = IterableContainer('Yaruo', 'Yaranaio', 'Yarumi')
>>> 
>>> # for 文
>>> for e in iterable_container: e
... 
'Yaruo'
'Yaranaio'
'Yarumi'
>>> 

 


方針

マニュアルに記載された通り、指定されたオブジェクトメソッドを実装します。
 

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

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

 

Step2. iterator オブジェクト

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

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

 

IterableContainer, MyIterator

class IterableContainer():
  def __init__(self, name1, name2, name3):
    self.name1 = name1
    self.name2 = name2
    self.name3 = name3

  # conteiner.__iter__() 
  def __iter__(self):
    return MyIterator(self)


class MyIterator():
  def __init__(self, iterable_container):
    self.name1 = iterable_container.name1
    self.name2 = iterable_container.name2
    self.name3 = iterable_container.name3

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

  # iterator.__next__()
  def __next__(self):
    if self.name1 is not None:
      tmp = self.name1
      self.name1 = None
      return tmp
    elif self.name2 is not None:
      tmp = self.name2
      self.name2 = None
      return tmp
    elif self.name3 is not None:
      tmp = self.name3
      self.name3 = None
      return tmp
    else:
      raise StopIteration('No name is stocked...')

補足

わざわざ iterable なオブジェクトと iterator オブジェクトを分けて設計するよう指示しているのは、 iterate する処理を iterable なオブジェクトの中で記述させないためなのかな... list, dictionary, string クラスも iterator の処理は、それぞれ専用のクラス list_iterator, dict_keyiterator, str_iterator クラスが用意され、別に記載されています。

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

 

あるいは Java での Iterator パターンに倣ってるだけなのかもしれない。Python のような動的言語の場合、Java のように別にクラスを分けなくても(interface の実装したりしなくても)、__next__ メソッドさえ実装していれば for 文に突っ込めるはずですが。この辺、動的言語は便利ですよねー。Storategy パターンなんかも別に、わざわざクラス分けたりしなくて良いので。Java 触ったことないですが...。

いずれにせよ iterable なオブジェクトを作らなくても実装できます。何故なら iterator オブジェクト自体も iterable な オブジェクトだからです。iterator オブジェクト自体にも __iter__ を実装するよう指示していることに着目してください。

isiterable 関数

__iter__ メソッドが実装されていれば iterable であると言えそうです。iterable なオブジェクト以外で __iter__ メソッドが実装されていないのであれば、下記の関数で判定できそうです。

__iter__ メソッドが実装されているかどうかの判定。具体的に言えば for ~ in ... 文で使えるかどうかの判定を行っています。

def isiterable(obj):
  return hasattr(obj, '__iter__') \
    and callable(getattr(obj, '__iter__'))

補足

正直 iterator の定義としては StopIteration を吐けって書いてあるけど iterable であるかどうかでは、そうも書いてないし。for 文の説明とか読んでると __next__ が1回でも実行できれば iterable と言えるみたいだし、 StopIteration を吐かなくても iterable と言えるみたい。

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

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

式リストは一度だけ評価され、これはイテラブルオブジェクトを与えなければなりません。 ... 中略 ... それぞれの要素は通常の代入規則でターゲットリストに代入され、その後スイートが実行されます。 (代入規則は 代入文 (assignment statement) を参照してください。) 全ての要素を使い切ったとき (シーケンスが空であったり、イテレータが StopIteration 例外を送出したなら、即座に)、 else 節があればそれが実行され、ループは終了します

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

isiterator 関数

iterator が正しく動作するかの判定を行います。端的に言えばマニュアルに記載された iterator の仕様を満たしているかどうかの判定。

間違っている場合は False を返します。それ以外の誤りを指摘するような詳細なエラーメッセージは出力しません。

使い方

>>> isiterator(IterableContainer('Yaruo', 'Yaranaio', 'Yarumi'), 3)
True
>>> 
>>> isiterator(IterableContainer('Yaruo', 'Yaranaio', 'Yarumi'), 2)
False
>>>
>>>
>>> isiterator([1, 2, 3], 3)
True
>>> 
>>> isiterator([1, 2, 3], 2)
False

isiterator

from copy import deepcopy


def isiterator(obj, 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__()
  if not hasmethod(obj, '__iter__'): return False  
  iterator = obj.__iter__()


  # 2) iterator.__iter__()
  # Does this function return self?
  if not hasmethod(iterator, '__iter__'): return False
  if not iterator is iterator.__iter__(): return False


  # 3) iterator.__next__()
  # After popping all elements,
  # does this function raise StopIteration?
  if not hasmethod(iterator, '__next__'): return False
  itr = deepcopy(iterator)
  for k in range(num_of_elements+1):
    try:
      itr.__next__()
   
    except StopIteration:
      # 3-1) raised StopIteration, 
      #      but some elements still remain.
      if k < num_of_elements: return False

      # success
      # will break from this for-statement
      if k == num_of_elements: pass

    except:
      # 3-2) raised an error,
      #      but the error is not StopIteration
      return False

    else:
      # 3-3) object method __next__ called,
      #      but more than num_of_elements
      if k == num_of_elements: return False

  return True


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

 




Remove all ads