フューチャー技術ブログ

イテレータと仲良くなろう

SAIG の佐藤尭彰です。最近は業務で Python ばっかり書いています。

今回は Python連載 の第4回目で、Python の中でも「なんとなく」で扱われがちなイテレータについてです。

イテレータとは

あるコンテナの中の要素に1つずつアクセスできるオブジェクト。

もう少し 公式 から引用すると、

(iter()) 関数は、コンテナの中の要素に1つずつアクセスする __next__() メソッドが定義されているイテレータオブジェクトを返します。

つまり、コンテナの中身を1つずつ返す __next__() メソッドを持つ (ようなオブジェクトを返す __iter__() 関数を持つ) ことがイテレータの本質です。

list などのシーケンスと異なり、実態として中身が存在する必要がありません。これを実装するための1手段が ジェネレータジェネレータ式 であり、返すべき値はこれらを呼び出すたびに都度計算してよいのです。このイテレータの性質から、イテレータを使えるところ (= iterable を要求されるところ) にシーケンスでなくイテレータを渡すとメモリや実行時間を削減できます。

一方でイテレータは実際に値が帰ってくるまでは中身が確定しません。確定させるためには list もしくは tuple などにキャストする必要があります。

>>> print(map(lambda x: x**2, range(5)))
<map object at 0x7f594ded2a58>
>>> print(list(map(lambda x: x**2, range(5))))
[0, 1, 4, 9, 16]

▲ ざっくり printf デバッグをするときに忘れがちな list()

組み込みのイテレータ

組み込み = import なしに使える、標準のもの

よく見るもの

ここは使いこなしているユーザが多いのではないでしょうか。

  • map(func, iterable): 第2引数に第1引数を作用させたものを返す
  • filter(func, iterable): 第2引数に第1引数を作用させた結果が真値となる要素のみを返す
  • enumerate(iterable): インデックスと中身のタプルを返す
  • zip(iter1, iter2, ...): それぞれの iterable の i 番目からなるタプルを返す
    • 長さがまちまちなときは最短なもので止まる

あまり見ないけど便利なもの

本題その1です。

  • filter(None, iterable)
    • 第1引数にNoneを渡すと、iterable内の要素自体が真値を返すような要素のみを返します
    • つまり「None, False, 0(と等価なもの), '', [], etc. 」を除くことができます。0が消えることを除いては かなり使い勝手がよく、無為な if 文でインデントを1つ掘るよりも見通しの良いコードを書くことができます
# やりがちな例
for v in iter:
if v:
process(v)

# ↑と等価な例
for v in filter(None, iter):
process(v)
  • reversed(seq)
    • いわゆるリバースイテレータを返します。逆順にしたコピーを返す [::-1] よりも軽くて便利なことが多いです
    • 一方で、(事実上)引数seqlisttupleである必要があります
  • iter(callable, sentinel): 2引数版 iter
    • sentinel と一致するまで callable を叩いた返り値を返します
    • 文字通りの番兵がいるようなテキストデータ・バイナリデータをパースするときに役に立つ(かも)

itertools

本題その2です。

import することでいろいろなイテレータが作れます。どこで使うんだと言うのもありますが、大体はいつか使える関数です。

主な無限イテレータ

  • count(start[, step=1]): stop がない無限 range
  • cycle(iterable): cycle('ABCD') --> A B C D A B C D A B C D ...
    • だいたい zip などの 一番短いやつに揃えて止まる 系ジェネレータを止めたくないときに使います

主な(普通の)イテレータ

  • accumulate(p[, func])
    • 累積和。np.cumsum(v)
    • このほか第2引数 func は幅広い二項演算を取ることができるため np.cumprod にしたり累積 max したり色々できます
  • chain.from_iterable(iterable)
    • 2重のネストに限定された np.ravel です
      • ネストされていない要素が混じっていたり、3重ネスト以上を平坦化したい場合はおとなしく collections.abc.Iterable かどうかを判定するしかないようです
from itertools import chain
from collections.abc import Iterable
a = [[1], [2, [3, 4], 5], [6]]
b = [1, [2, [3, 4], 5], 6]

def flatten(it):
for item in it:
if isinstance(item, Iterable) and not isinstance(item, str):
for child in flatten(item):
yield child
else:
yield item

list(chain.from_iterable(a)) # => [1, 2, [3, 4], 5, 6]
list(flatten(a)) # => [1, 2, 3, 4, 5, 6]
list(chain.from_iterable(b)) # => TypeError: 'int' object is not iterable
list(flatten(b)) # => [1, 2, 3, 4, 5, 6]

▲ こんな感じで組むと flatten できる

  • groupby(iterable[, key])
    • 前から見ていって key が一致するような要素集合を (key, 要素集合) の形で返します
    • iterable がソート済ならばだいたい df.groupby ですが、ソートされていないと key が変わるたびにブロックを返すので敢えてそれを利用する使い道もあります
      • タイムスタンプ順にそろえておいて、同じ人の連続ログをひとまとめにして見たい、など
      • C++ の uniq と同じような動作です
  • islice(iterable[, start], stop[, step])
    • iterable[start:stop:step]
    • reversed 同様、コピーを作らないのでメモリに優しいです
  • takewhile(pred, seq)
    • pred が偽になったら終了するイテレータ
    • リスト内包でbreakしたくなったらこれを思い出して下さい

おわりに

標準ライブラリを上手に使って快適な Python ライフを。

次回は10月7日、小橋昌明さんの pandasの内部で何が起きているか です。