【Python】 リスト内包表記の基本

Python リスト 組込み型

リスト内包表記は、リストを生成するPythonのコードの書き方の一つです。
同じ動作をfor文で書いた場合と比較して、適切に利用すればコードの可読性や実行速度の点で有利であると言われています。
本記事では、

  • リスト内包表記の基本(for文と内包表記の構文の違いに着目)
  • 内包表記のいろいろな具体例
  • リストだけでなく、ジェネレータ式、辞書、集合の内包表記の作り方

について解説します。
また最後に、

  • リスト内包表記は本当に早いのか?

について、実際にfor文やmap()との処理時間比較と併せてまとめています。興味があればご参照ください!

リスト内包表記とは

リスト内包表記は、リストから新しい別のリストを作ることができるPythonの記法の一つです。
例えば、下記のような場面に使えます。

  • あるリストの要素それぞれに対して、何らかの操作を行って新しいリストを作る
  • 例)各要素の文字列型を数値型に置き換える
    [‘1’, ‘2’, ‘3’] ==> [1, 2, 3]

  • 条件に合う要素を抜き出して新しいリストを作る
  • 例)リストから偶数のみを抜き出す
    [1, 2, 3, 4, ,5 ,6] ==> [2, 4, 6]

これらはfor句を用いた繰り返し文を使って書くことも出来ますが、内包表記を使うと実行効率が良くなる等の利点があります(後述)。

確認した環境

  • OS: Ubuntu18.04LTS
  • Python3.7.4

リスト内包表記の書き方

リスト内包表記の書式

リスト内包表記は、下記に示すようにfor節(for … in …)、そして0個以上の for節かif節から構成された式(Expression)を、括弧[]で囲ってリスト化したものです。
ちなみに、これを()で囲えばジェネレータ内包表記、{}で囲えば辞書や集合内包表記になります(後述)。

list comprehension format

ここでは、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句を入れ替えるような)構造になります。

    list comprehensions

後置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文が後置配置となっています。

    list comprehensions with if statement

条件式(三項演算子)

条件式(三項演算子)の場合は、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']

    この構文の構造を下図に示します。条件式が前に配置されています。

    list comprehensions with conditional expression

(※)Python3.8からは代入式 が導入されました。こちらは別途まとめる予定です。

内包表記のいろいろな例

for文で書けるものは内包表記で書くことができます。この章では、ネストや複数の条件分岐など複雑な構文を内包表記で書いてみます。
但し、可読性を損なう場合もありますので使いすぎに注意ですね。

forループのネスト

要素の組み合わせを網羅する場合は、forによる繰り返し文をネストすることで書くことが出来ます。ここでは、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文との構造比較を下記に示します。

    list comprehension with for loops

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句ひとつひとつを内包表記で書いていきます。

    list comprehensions with nest of for loop

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]

    list comprehension with for loop

複数の条件分岐(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]

    条件式を見つけることができれば、基本の構造通りです。

    list comprehension with some conditional expressions

    但し、上記の例程度の条件分岐であればまだ読むことは可能ですが、もっと複雑な条件分岐(例えば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}

参考記事:【Python】 辞書(dict)の使い方の基本

集合内包表記

集合(set)型を表す”{ }”で囲います。辞書との違いは、キーと値を分けるコロン(:)が無いことです。

>>> a = {2*n for n in range(5)}
>>> a
{0, 2, 4, 6, 8}

# データ型の確認
>>> type(a)
<class 'set'>

参考記事:[Python] 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ループを使った記述のこと)

また、こちらのサイト様は内部動作まで踏み込んで詳しい分析をされていますので、ご参考まで。

まとめ

今回は、Pythonでよく使われるリスト内包表記の基本についてまとめました。

  • リスト内包表記とは、リストから新しい別のリストを作ることができるPythonの記法の一つです。
  • for句を使った繰り返し文を内包表記を使った式(Expression)に置き換えることが出来ます。
  • 適切に使うことでコードの可読性、および実行効率の改善が期待出来ますが、使いすぎると逆に読みにくいコードになるので注意が必要です。

参考資料)公式リファレンス:5.1.3. リストの内包表記

Learn more...

書籍でもう少し詳しく学びたい場合はこちらもどうぞ。筆者もかなり参考にさせてもらっています!

シェアする
ひびきをフォローする
Hbk project