DjangoBrothers BLOG ✍️

2019/07/05

このエントリーをはてなブックマークに追加
Python パフォーマンス向上

【Python】はじめてのリスト内包表記(List Comprehensions)

Pythonの特徴的な機能として、リスト内包表記(List Comprehensions)があります。

リスト内包表記は、他の言語を扱っていた方やPython初心者の方からすると、ちょっとだけ理解するハードルがあると思いますが、一度理解してしまえばリストの作成をすっきり書くことができ、実行速度の面でもメリットのある書き方です

実際の使い方や実行速度の比較をみて、その便利さを体感してみましょう。

通常のリストの作成

まずは通常のリストを用いて、Iterable(for文で繰り返すことのできるオブジェクト)な変数を作成してみましょう。

for文でdoublesというリストを作成

>>> doubles = []
>>> for x in range(10):
...         doubles.append(x*2)

>>> doubles
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

ここでは、要素をそれぞれ2倍したdoublesというリストを作成しています。これがfor文を利用したリストの作成の基本型です。

リスト内包表記

先程のコードと同じことを、リスト内包表記バージョンで確認してみましょう。以下のように書くことで同じ処理を実現できます。

リスト内包表記でdoublesというリストを作成

>>> doubles = [x*2 for x in range(10)]
>>> doubles
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

リスト内包表記では1行でかけてしまいました。構成を少し解説します。 リスト内包表記の基本的な構成は以下のようになっています。

リスト内包表記の基本的な構成

[<変数Aを用いた処理> for <変数A> in <Iterableなオブジェクト>]

まず、リストの一番右側に書かれている、Iterableなオブジェクトとは、簡単にいうとfor文で要素を一つ一つ取得して処理できるオブジェクトのことです。 例えば、わかりやすいのは、リスト型、String型があります。

このIterableなオブジェクトに対して、for <変数A> in <Iterableなオブジェクト>の部分で一つ一つの要素を<変数A>に代入しています。

そして最後に、リストの表記の一番左側の<変数Aを用いた処理>で要素に対して処理を行い、それをリストに一つ一つ追加する、というのがリスト内包表記の全体像です。 ここでの、要素に対する処理というのは、上で示したサンプルでいう2倍にする処理などを指します。もちろんこれは、何も処理せず、各要素をそのまま格納することもできます。

基本がわかったところで、リスト内包表記の応用もみてみましょう。

for文を二つ重ねる

リスト内包表記では、for文を重ねることもできます。 少し複雑になりますが、やっていることは同じです。複数のパターンを試してみて、なれるようにしてください。

for文を二つ重ねる

>>> matrix = [
...     [1, 2, 3],
...     [4, 5, 6],
...     [7, 8, 9]
... ]
>>> [num for row in matrix for num in row]
[1, 2, 3, 4, 5, 6, 7, 8, 9]

これもやっていることは一緒です。 まず、一番最初のnumで最終的に取り出したい要素を置いておきます。

そしてfor row in matrixでは、matrixという変数から一つ一つの要素を取り出してrowという変数に格納しています。ここで、matrixの一つ一つの要素とは、[1, 2, 3][4, 5, 6][7, 8, 9]の三つを表しています。

最後のfor num in rowという部分は、先程取り出したrowに入っている変数を一つ一つ取り出しながら、numという変数(一番最初に置いた変数)に格納しています。 例えば、rowの中の要素の一つである、[1, 2, 3]を対象にすると、123という要素を取り出しています。

最終的に、それらの数字をリストに順番に追加しているという処理になります。

if文も使った方法

リスト内包表記ではif文も利用できます。 サンプルをみてみましょう。

偶数だけをリストにする

>>> even_nums = [x for x in range(10) if x % 2 == 0]
>>> even_nums
[0, 2, 4, 6, 8]

これは0から9までの数字の中で、偶数の条件に当てはまるものだけをリストに含める処理です。

これまでのリスト内包表記の基本型に加えて、最後の部分にif文を追加することで、取得した要素(ここではx)に対して、特定の条件が当てはまる時にだけ、リストの要素に追加するということを行います。

また、ifだけではなく、if-elseを用いた書き方もできます。

偶数はそのまま、奇数には「奇数」という文字列を入れる

>>> odd_num_to_string = [x if x % 2 == 0 else '奇数' for x in range(10)]
>>> odd_num_to_string
[0, '奇数', 2, '奇数', 4, '奇数', 6, '奇数', 8, '奇数']

if-else文を使うときは、ifだけを利用した場合と比較すると、条件判定の位置が異なるので気をつけてください。

実行速度の比較

リスト内包表記は、単純なfor文を使ったリストの作成よりもシンプルにリストを作成できるというメリットだけではありません。

実は、リストを作成する際の実行速度の面でも優位性があります。 以下のようなPythonファイルを作成してみて、簡単に試してみましょう。

test.py

import time

def func1():
    nums = []
    for i in range(100000):
        nums.append(i)

def func2():
    nums = [i for i in range(100000)]

if __name__ == '__main__':
    start_1 = time.time()
    func1()
    end_1 = time.time()
    print(f"func1の処理時間:{end_1 - start_1}s")

    start_2 = time.time()
    func2()
    end_2 = time.time()
    print(f"func2の処理時間:{end_2 - start_2}s")

consoleで実行結果の確認

$ python test.py
func1の処理時間:0.01638031005859375s
func2の処理時間:0.0060689449310302734s

同じ処理を行いましたが、リスト内包表記を利用した場合の方が実行速度が速いことがわかります。 これは、for文を利用したリストの作成では、Pythonのappendメソッドを毎回呼び出すことになるので、その分のオーバーヘッドが原因です。

実際の仕事などで使用する場合にも、なるべくリスト内包表記を利用した方が処理効率の高いプログラムを書いていることになるので、ぜひ積極的に利用していきましょう。

まとめ

まとめると、リスト内包表記のメリットは、以下の二つです。

  • リストをすっきりした構文で記述できる
  • 実行速度が速く、効率的なプログラムを書くことができる

リスト内包表記はPythonの初心者の方には少しハードルが高く見える書き方ですが、一度基本を理解してしまえば非常にシンプルな記法です。 また、上記のようなメリットも享受できるので、ぜひ積極的に活用していきましょう。