今更ながら1ではありますが、この記事では内包表記の基礎と応用について説明します。
例
リスト内包表記を用いると簡潔にリストの要素を定義できます。
a = [x for x in hoge] # hogeはイテラブル
これが リスト内包表記 (List Comprehensions) です。リストリテラル []
の内部に、要素ではなく forループが記述されているような文法をとります。
なお、上で示したリスト内包表記 と for文を用いて書いた次のコード は等価です。
a = []
for x in hoge:
a.append(x)
利点
リスト内包表記の利点はなんと言ってもその シンプルさ です。空のリストを作成して、for文で回して、最後尾に追加する、という通常3行で書く処理を 単一行で処理可能 です。
デバッガのウォッチ式で活用すれば、データオブジェクトから複雑な抽出を行うことも可能になります。
さまざまな内包表記
本記事では「リスト内包表記」を主題としていますが、タプル、辞書、集合、にも内包表記は存在します。タプルと集合の内包表記はリストとほとんど変わらないため割愛します。
辞書内包表記
d = {k: v for k, v in zip(keys, values)} # keysとvaluesはイテラブル
辞書のキーと値を別々のリスト(keys
, values
)で保持している際に、それらを統合した新規の辞書を作成することができます。
zip()
は複数のイテラブルをまとめる反復可能な組み込みクラスです。for文で用いるとそれぞれの要素をタプルにまとめて順番に返します。要素数が一致しない場合、余剰分は破棄されます2。
二重の内包表記
内包表記に含まれる for
の数に制限はありません。次のように for に for を重ねることで入れ子構造を作ることができます。これは多次元配列の展開や行列演算に役立ちます。
b = [i for l in [[1, 2],[3, 4]] for i in l]
# [1, 2, 3, 4]
この内包表記をfor文で書き下ろすと次のようになります。
b = []
for l in [[1, 2],[3, 4]]:
for i in l:
b.append(i)
二重の内包表記は、親ループがどれなのか混同しやすいので注意が必要です。
先に書いた for
が親ループ になります。図で表すと次のとおりです。
for
で書き下した場合と同様に、前方に配置された for
のスコープは以降に引き継がれます。先頭に配置する実際に追加する要素からはすべての変数が参照できます。
この章の初めに説明したとおり for
の数に制限はありませんので、次のような内包表記も実行可能です。(ここまでやってしまうとPEP8の文字数制限にも違反し可読性が低下するため現実的ではありませんが。)
# 任意のイテラブル hoge, fuga, piyo, hogera, hogehoge に対して
[(i, j, k, l, m) for i in hoge for j in fuga for k in piyo for l in hogera for m in hogehoge]
条件分岐
内包表記では要素の追加を条件分岐できます。
[i for i in range(50) if i % 5 == 0]
# [0, 5, 10, 15, 20, 25, 30, 35, 40, 45]
最後に if
を置くことで 条件が True
のときのみ要素をリストに追加することができます。条件が評価されるのは最後尾の for
通過時です。
速度
実行速度も調べてみます。以下の条件で測定し、平均値と標準偏差を numpy
を用いて算出します。
データ分析に関しては素人でございますので手法に誤りがあるかもしれません。何かお気づきの点がありましたらコメント欄からご教示ください。
条件
- 測定では、後に示す「リスト内包表記コード」あるいは「for文による書き下しコード」に
range(1000)
を与え新規のリストa
を作成します。この処理を 1回 といいます。 - 開始時刻と終了時刻を
time.time()
で取得し、その差分を 実行時間 とします。 - 前述の処理を 10回 行い、その実行時間の平均値と標準偏差を 結果 とします。
- 実行環境は Python 3.8.6, MacOS Ventra13.4.1 です。
ソースコード
import time
import numpy
t = [[], []]
# 内包表記
for _ in range(10):
t0 = time.time()
a = [i for i in range(1000)]
t[0].append(time.time() - t0)
# for文
for _ in range(10):
t0 = time.time()
a = []
for i in range(1000):
a.append(i)
t[1].append(time.time()-t0)
# numpy配列に変換
us_comprehension = numpy.array(t[0]) * 1_000_000
us_for = numpy.array(t[1]) * 1_000_000
# 結果
print(f"Comprehension: {us_comprehension.mean():.3f}us, {us_comprehension.std():.3f}us")
print(f"for statement: {us_for.mean():.3f}us, {us_for.std():.3f}us")
結果
結果は 平均値 / 標準偏差 で示しています。単位はマイクロ秒です。
No. | リスト内包表記 | for文による書き下ろし |
---|---|---|
1 | 27.800 / 3.147 | 72.074 / 1.806 |
2 | 27.919 / 2.917 | 73.385 / 0.531 |
3 | 27.204 / 3.178 | 71.120 / 1.781 |
4 | 28.539 / 4.453 | 73.290 / 0.467 |
5 | 29.111 / 5.640 | 73.314 / 2.262 |
いずれの結果もリスト内包表記の圧勝です。Pythonはインタプリンタ言語ですのでステップ数が少ないほど速度が上がることは予測できたところではあります。Pythonでリストを扱うなら内包表記の利用をまず考えてみるといいかもしれません。
おわりに
長くなりましたが最後までご覧いただきありがとうございました。
この記事を書くに思い立ったのは他でもなく私が内包表記の大ファンだからです。内包表記を実装しているプログラミング言語は実はごく少なく、いわばPythonを扱う我々の特権とも言えるわけです。
「意味を捉えづらい」として一部からは批判にさらされることもある内包表記ですが、数学の集合表記を彷彿とさせる合理的な構文で、偉大な発明であると思っています。
これからも内包表記をはじめとした「Pythonicな」コーディングを楽しんでいきたいですね!