Pythonのenumerateの挙動を理解する
これはなに
Effective Python 第2版の項目7「rangeではなくenumerateを使おう」において、下記の記述があった。
enumerate
は、遅延評価ジェネレータでイテレータをラップします。
enumerate
は、ループのインデックスとイテレータの次の値の対をyield
します。
この記述の意味がさっぱりわからなかったので調べた。
enumerate
とは
そもそもenumerate
はPythonの組み込み関数のひとつである。
enumerate(iterable, start=0)
引数
引数 | 名前 | 渡すもの |
---|---|---|
第1引数 | iterable |
シーケンス、イテレータ、あるいはイテレーションをサポートするその他オブジェクト |
第2引数 | start |
enumerate() が返すカウントの初期値 |
戻り値
enumerate()
はイテレータを返す。
enumerate()
が返すイテレータの__next__()
メソッドは、start
からのカウントと、iterable
のイテレーションによって得られた値を含むtuple
を返す。
使い方
主に、イテラブルなオブジェクトをイテレータでループしつつ要素のインデックスを取得したい場合に使う。たとえば、下記のように利用すると、list
をループさせつつ要素のインデックスを取得できる。
languages = ["Python", "Golang", "VHDL", "C++"]
for idx, lang in enumerate(languages):
print(f"{idx + 1} : {lang}")
上記コードを実行すると、下記出力が得られる。
1 : Python
2 : Golang
3 : VHDL
4 : C++
記述を理解するための知識を得る
enumerate
は、遅延評価ジェネレータでイテレータをラップします。
enumerate
は、ループのインデックスとイテレータの次の値の対をyield
します。
という記述を理解するためには、下記5つの単語を理解しなければならない。
- 遅延評価
- イテレータ
- ジェネレータ
-
yield
する - ラップする
キーワードの要点
- 遅延評価: 値が要求されるまでその評価を遅らせること。
- イテレータ: 要素を1つずつ順番に取り出せるオブジェクト。
__iter__()
と__next__()
を持つ。 - ジェネレータ: 遅延評価の性質を持つイテレータ。
yield
を使用して生成する。 -
yield
: ジェネレータ関数内で使用され、関数の状態を一時停止し、値を返す構文。 - ラップ: あるオブジェクトを取り囲んで新しい機能や情報を提供すること。
遅延評価とは
遅延評価(Lazy Evaluation)とは、プログラムの評価を値が必要になるまで遅らせる手法である。言いかえると、値が要求されるまでその評価(計算や取得)を行わない方法である。対義語は先行評価[1]。
たとえば、通常のlist
の場合、変数定義時にすべての要素が計算されてメモリに格納される。これは先行評価である。
# リストを生成
squares = [x * x for x in range(10)]
# この時点で、`squares`には10個の要素が計算済みの状態で格納されている
print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
一方で、ジェネレータを利用すると、next
関数を使って値を要求するたびに次の値を生成する。これは遅延評価である。
# ジェネレータを生成(ジェネレータ式)
squares_generator = (x * x for x in range(10))
# この時点では、`squares_generator`にはジェネレータオブジェクトが格納されている
# そのため、`squares_generator`をprintしても、値は得られない
print(squares_generator) # <generator object <genexpr> at 0x7f4decfd42b0>
square_value = next(squares_generator) # ここで初めて1つ目の値が評価される
print(square_value) # 0
先行評価を利用した場合は、評価されたすべての値が同時にメモリ上に存在する。一方で、遅延評価を利用した場合は、値が要求されるたびに次の値を生成する。
そのため、遅延評価ではすべての値を同時にメモリ上に存在させなくてすむ。ゆえに、遅延評価を利用すると、大量のデータを扱う場合でもメモリ効率が良くなる。たとえば、数千万、数億の数字を2乗する場合、リスト内包表記を使うとメモリが圧迫されてしまう。しかし、ジェネレータを使用すれば、メモリを節約しながら1つずつ処理できる。
イテレータとは
イテレータとは、要素を1つずつ順番に取り出せるオブジェクトである。イテレータは下記2つのメソッドを持つことが要求される。
-
__iter__()
: イテレータ自身を返す -
__next__()
: 次の要素を返す。次の要素がない場合はStopIteration
の例外を投げる
たとえば、list
に対してiter()
関数を用いると、イテレータを取得できる。
my_list = [1, 2, 3]
# リストからイテレータを取得
iterator = iter(my_list)
# イテレータを使って要素を1つずつ取り出す
print(next(iterator)) # 1
print(next(iterator)) # 2
print(next(iterator)) # 3
# 要素がなくなった時点で、StopIteration例外が発生
# print(next(iterator))
iter()
関数でイテレータを取得できるオブジェクトはイテラブル(iterable)と呼ばれる。list
, tuple
, str
などはiterableである。enumerate
はこのiterableを第1引数にとる。
ジェネレータとは
Pythonにおけるジェネレータとは、遅延評価の性質を持つ特別なイテレータの一種である。
ジェネレータはイテレータであるため、__iter__()
と__next__()
の2つのメソッドを持つ。また、遅延評価の性質を持つため、__next__()
メソッドが呼ばれるまで次の要素を生成しない。__next__()
メソッドが呼ばれてはじめて次の要素を生成する。
基本的なジェネレータ関数の例を以下に示す。これは、0からlimit
に指定した数までの整数を順に返すジェネレータを生成する。
def count_up_to(limit):
count = 0
while count <= limit:
yield count
count += 1
counter = count_up_to(5)
for number in counter:
print(number) # 0 1 2 3 4 5
yield
するとは
ジェネレータ関数では、return
の代わりにyield
を使う。yield
を使うと、関数はyield
文の実行後に処理を一時停止して、内部状態を保存するように動作する。
たとえば、下記のジェネレータ関数では、count
の値をyield
している。yield
を利用しているため、関数を実行してもwhile
文はすべて実行されず、yield
文が実行された時点で動作を一時停止する。
def count_up_to(limit):
count = 0
while count <= limit:
yield count
count += 1
counter = count_up_to(5)
print(next(counter)) # 0
# `yield count`の行が実行され、0が返される
# `count_up_to`内の`count`の値は0のまま一時停止する
print(next(counter)) # 1
# この実行ではじめて`count += 1`が評価されている
# その後`yield count`の行が実行され、1が返される
# `count_up_to`内の`count`の値は1のまま一時停止する
つまり、「yield
する」とは、処理をすべて実行せずに、必要な処理を終えたら一時停止するようにするということである。
ラップするとは
「ラップする」とは、何かを取り込み、その上で追加の機能や情報を提供する新しいオブジェクトを作成することを意味する。なにかのオブジェクトを上から包むようなイメージである。
たとえば、「イテレータをラップする」という表現は、元のイテレータをそのままの形で返さず、何らかの追加情報や変更を加えて新しいイテレータを提供することを指す。
記述を理解する
enumerate
は、遅延評価ジェネレータでイテレータをラップします。
enumerate
は、ループのインデックスとイテレータの次の値の対をyield
します。
という記述は、enumerate
が順を追うと下記の動作をするということを表している。
-
enumerate
が第1引数でiterableを受け取る - 受け取ったiterableのイテレータをジェネレータでラップし、新しいジェネレータイテレータを返す
- ジェネレータイテレータは、呼ばれるたびにループのインデックスとイテレータの次の値のペアを
yield
して返す
公式ドキュメントによると、enumerate
は下記関数と等価だと記されている。これは上記記述を簡潔に表している。
def enumerate(iterable, start=0):
n = start
for elem in iterable:
yield n, elem
n += 1
ちなみに、文書中の「遅延評価ジェネレータ」とは、Pythonにおいてはジェネレータそのものである。なぜなら、Pythonのジェネレータは__next__()
メソッドを呼んではじめて次の要素を評価する遅延評価の性質を持つためである。
参考文献・URL
-
ちなみに、遅延評価/先行評価という単語は、決してPython特有のものではない。 ↩︎
Discussion
素晴らしい記事をありがとうございます😃!
enumerate
の使いどころってどんなシーンでしょうか?巨大な配列を順番に処理していくときには
__next__
を使っていたのですが、enumerate
の方が簡潔に書けそう?🤔コメントありがとうございます。
enumerate
は、list
やtuple
などをループさせつつ要素のインデックスを得たい場合に有用です。これは、本記事を書くきっかけとなったEffective Python 第2版の項目7「rangeではなくenumerateを使おう」にも同様の記載があります。たとえば、
list
をループさせつつ要素のインデックスを得たい場合、下記のように、enumerate
を使うことで簡潔に処理できます。文字列の文字とその位置を取得するような場合も有用です。
巨大な配列を順番に処理していく場合に
enumerate
を使うと簡潔に書けるかということについては、その処理で要素のインデックスも得たいかどうかに依るかと思います。要素のインデックスも取得したいならenumerate
は有用ですが、そうでないなら使う理由はないでしょう。なるほど!ありがとうございます😊🌟
今までzipとnext(スマホなのでアンスコでない…)使ってました。確かに便利です!
ありがとうございます!😆🌟