リスト内包表記は、リストを生成するPythonのコードの書き方の一つです。
本記事では、
- リスト内包表記の基本的な使い方
- 内包表記のいろいろな具体例
- リストだけでなく、ジェネレータ式、辞書、集合の内包表記の作り方
について、for文との構文の違いに着目しつつ、具体例をあげて解説します。
また、for文と比較して処理効率が良いとも言われていますので、実際に処理時間を比較してみました。
リスト内包表記とは
リスト内包表記は、リストから新しい別のリストを作ることができるPythonの記法の一つです。
例えば、下記のような場面に使えます。
- あるリストの要素それぞれに対して、何らかの操作を行って新しいリストを作る
- 条件に合う要素を抜き出して新しいリストを作る
例)各要素の文字列型を数値型に置き換える
[‘1’, ‘2’, ‘3’] ==> [1, 2, 3]
例)リストから偶数のみを抜き出す
[1, 2, 3, 4, ,5 ,6] ==> [2, 4, 6]
これらはfor句を用いた繰り返し文を使って書くことも出来ますが、内包表記を使うと実行効率が良くなる等の利点があります(後述)。
リスト内包表記の基本
リスト内包表記の書式
リスト内包表記は、下記に示すように
- 式
- for節(for … in …)
- 0個以上の for節かif節
から構成された式(Expression)を、括弧[ ]で囲ってリスト化したものです。
※)これを()で囲えばジェネレータ内包表記、{}で囲えば辞書や集合内包表記になります(後述)。
ここでは、forによる繰り返し文と書き方を比較しながらリスト内包表記の書き方を見ていきます。
- forによる繰り返し文
>>> a = []
>>> for n in range(5):
... a.append(n)
...
# 結果
>>> a
[0, 1, 2, 3, 4]
>>> a = [n for n in range(5)]
>>> a
[0, 1, 2, 3, 4]
下記の様に、式とfor句を入れ替えるような構造になります。
後置ifによる条件分岐
else節の無いif文による条件分岐がある場合は、for句の後ろに記載します(後置if)。
例えば、ある条件を満たした要素のみを抽出して新たなリストを作る場合に使います。
上記と同様にfor文と比較します。
- forによる繰り返し文
>>> a = []
>>> for n in range(10):
... if n%2 == 0:
... a.append(n)
...
# 結果
>>> a
[0, 2, 4, 6, 8]
>>> a = [n for n in range(10) if n%2 == 0]
>>> a
[0, 2, 4, 6, 8]
for文と内包表記の構造の違いを下図に示します。if文が後置配置となっています。
条件式(三項演算子)
elseがある条件式(三項演算子)の場合は、for句の前に置きます。
条件によって式の評価結果を変更したい場合に使います。
- forによる繰り返し文
>>> a = []
>>> for n in range(10):
... if n%2 == 0:
... a.append(n)
... else:
... a.append('x')
...
# 結果
>>> a
[0, 'x', 2, 'x', 4, 'x', 6, 'x', 8, 'x']
>>> a = [n if n%2==0 else 'x' for n in range(10)]
>>> a
[0, 'x', 2, 'x', 4, 'x', 6, 'x', 8, 'x']
この構文の構造を下図に示します。条件式が前に配置されています。
(※)Python3.8からは代入式 が導入されました。こちらは別途まとめる予定です。
内包表記のいろいろな例
for文で書けるものは内包表記で書くことができます。
この章では、ネスト(入れ子)や複数の条件分岐など複雑な構文を内包表記で書いてみます。
但し、可読性を損なう場合もありますので使いすぎに注意です。
forループのネスト
2重のforループを内包表記にする方法です。
要素の組み合わせを全て取得したい場合に使えます。
- forによる繰り返し文
>>> a = []
>>> for i in range(2):
... for j in range(2):
... a.append([i,j])
...
>>> a
[[0, 0], [0, 1], [1, 0], [1, 1]]
>>> [[i,j] for i in range(2) for j in range(2)]
[[0, 0], [0, 1], [1, 0], [1, 1]]
for文との構造比較を下記に示します。
2次元配列リスト
2次元配列のリストもforループをネストすることで作ることが出来ます。
ここでは下記の2次元配列リストを内包表記で書いてみます
[[0, 1, 2, 3],
[0, 1, 2, 3],
[0, 1, 2, 3]]
- forによる繰り返し文
>>> a = []
>>> for i in range(3):
... a.append([])
... for j in range(4):
... a[i].append(j)
...
# 結果
>>> a
[[0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3]]
>>> [[j for j in range(4)] for i in range(3)]
[[0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3]]
下図の様に、ネストされたfor句ひとつひとつを内包表記で書いていきます。
3次元配列リスト
前項の応用として、3次元配列をリストで作成する場合の例を示します。
[[[0, 1, 2], [0, 1, 2], [0, 1, 2]],
[[0, 1, 2], [0, 1, 2], [0, 1, 2]],
[[0, 1, 2], [0, 1, 2], [0, 1, 2]]]
- for文
>>> a = []
>>> for i in range(3):
... a.append([])
... for j in range(3):
... a[i].append([])
... for k in range(3):
... a[i][j].append(k)
...
# 結果
>>> a
[[[0, 1, 2], [0, 1, 2], [0, 1, 2]], [[0, 1, 2], [0, 1, 2], [0, 1, 2]], [[0, 1, 2], [0, 1, 2], [0, 1, 2]]]
>>> [[[k for k in range(3)] for j in range(3)] for i in range(3)]
[[[0, 1, 2], [0, 1, 2], [0, 1, 2]], [[0, 1, 2], [0, 1, 2], [0, 1, 2]], [[0, 1, 2], [0, 1, 2], [0, 1, 2]]]
但し、ここまでネストされたループの内包表記は非常に複雑となりますので、避けたほうが良いかもしれません。
2次元から1次元への展開
2次元配列
[[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11]]
を下記の1次元配列に展開する場合のコードを内包表記で書いてみます。
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
- forによる繰り返し文
>>> a = [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]
>>> b = []
>>> for i in a:
... for j in i:
... b.append(j)
...
# 結果
>>> b
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
上記で記載した基本のforループのネストと同様の形です。
>>> [j for i in a for j in i]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
複数の条件分岐(if〜elif、else)
複数の条件分岐が繋がった場合も、条件式(三項演算子)を使うことで、内包表記を記述できます。
- forによる繰り返し文文
>>> a = []
>>> for i in range(5):
... if i == 0:
... a.append('a')
... elif i == 1:
... a.append('b')
... elif i == 2:
... a.append('c')
... else:
... a.append(i)
...
# 結果
>>> a
['a', 'b', 'c', 3, 4]
>>> ['a' if i==0 else 'b' if i==1 else 'c' if i==2 else i for i in range(5)]
['a', 'b', 'c', 3, 4]
条件式を見つけることができれば、基本の構造通りです。
但し、上記の例程度の条件分岐であればまだ読むことは可能ですが、もっと複雑な条件分岐(例えばforループのネストと複合になる等)になるとそれも難しくなることは想像できます。これについても、使いすぎないようにすることが必要ですね。
ジェネレータ式、集合、辞書の表示
前述のように、リスト内包表記は内包表記を括弧[]で囲ってリスト表示したものです。
同様にして、内包表記を使ってジェネレータや辞書、集合を表すことも出来ます。
ジェネレータ内包表記
リスト内包表記は、入力が大きい場合にメモリを使いすぎるという短所があります。
これに対してジェネレータを使うと、イテレータとして出力を一つ一つ生成するのでメモリを節約することが出来ます。
ジェネレータ式は、内包表記と同じ表記を丸括弧()で囲うことで生成できます。
>>> g = (i*2 for i in range(4))
# ジェネレータです。
>>> g
<generator object <genexpr> at 0x7f2f22375a50>
# for文で確認
>>> for i in g:
... print(i)
...
0
2
4
6
尚、丸カッコ()というとタプルが思い浮かびますが、この場合は上記の通りタプルにはなりません。
タプルは要素をカンマ(,)で区切ったもので表され、カッコ()は関係ありません(空タプルは除く)。
尚、タプル内包表記を生成したい場合は、tuple()を使います。
>>> a = tuple(i*2 for i in range(4))
>>> a
(0, 2, 4, 6)
# データ型を確認。タプルです。
>>> type(a)
<class 'tuple'>
参考記事:【Python】タプルの使い方の基本
辞書内包表記
キーと値をコロン(:)で繋いだペア key:value を”{ }”で囲います。
>>> fruits = ['apple', 'orange', 'grape']
>>> num = [4, 5, 6]
>>> {i : j for i, j in zip(fruits, num)}
{'apple': 4, 'grape': 6, 'orange': 5}
集合内包表記
集合(set)型を表す”{ }”で囲います。辞書との違いは、キーと値を分けるコロン(:)が無いことです。
>>> a = {2*n for n in range(5)}
>>> a
{0, 2, 4, 6, 8}
# データ型の確認
>>> type(a)
<class 'set'>
内包表記の処理速度
内包表記を用いるメリットとして、処理速度が挙げられます。ここでは、処理速度について実測データと併せて確認してみます。
以下は、forループ、map()、リスト内包表記の処理時間の比較グラフです。
※横軸:データ数、縦軸:処理時間(両軸とも基底10の対数値);(コードはこちら)
※条件:OS Ubuntu16.04LTS, Python3, PC: Core i3
処理時間の数値自身はPCによって異なるので無視頂くとして、相対的に比較するとリスト内包表記はforループやmap関数を使った場合と比較して約1.6倍速い結果となりました。
理由については、書籍「エキスパートPythonプログラミング改訂2版」に以下のように記載されています。
この書き方はC言語では良いかもしれませんが、次の理由のためPythonでは遅くなります。
- リストを操作するコードをループごとにインタープリタ上で処理する必要がある
- カウンタ操作もループごとにインタープリタ上で処理する必要がある
- append()はリストのメソッドであるため、イテレーションごとに関数ルックアップの追加のコストが必要になる
(中略)リスト内包表記を利用すると、先ほどの構文が行っていた処理の一部がインタープリタ内部で実行されるようになるので、処理が早くなります。
書籍「エキスパートPythonプログラミング改訂2版、アスキードワンゴ、MichalJaworski他著」
(注:「先ほどの構文」というのは、forループを使った記述のこと)
また、こちらのサイト様は内部動作まで踏み込んで詳しい分析をされていますので、ご参考まで。
確認した環境
- OS: Ubuntu18.04LTS
- Python3.7.4
まとめ
今回は、Pythonでよく使われるリスト内包表記の基本についてまとめました。
- リスト内包表記とは、リストから新しい別のリストを作ることができるPythonの記法の一つです。
- for句を使った繰り返し文を内包表記を使った式(Expression)に置き換えることが出来ます。
- 適切に使うことでコードの可読性、および実行効率の改善が期待出来ますが、使いすぎると逆に読みにくいコードになるので注意が必要です。
参考資料)公式リファレンス:5.1.3. リストの内包表記