『Python数値計算ノート』ではアフィリエイトプログラムを利用して商品を紹介しています。

内包表記

内包表記 (Comprehension)

Python にはリストディクショナリなどのイテラブルオブジェクトの各要素を操作して、新しいイテラブルオブジェクトを作り出す内包表記(Comprehension)とよばれる記法が備えられています。

リスト内包表記

list オブジェクトの操作は案外面倒なものです。たとえば、リストのすべての要素に 1 を加えた新しいリストを作ろうとすれば、for文を使って次のようなコードを書く必要があります。

# PYTHON_LIST_COMPREHENSION

# In[1]

# リスト型データを定義
num_list = [1, 2, 3, 4, 5]

# 空白のリストを用意します
new_list = []

# リストの各要素に1を加える
for x in num_list:
    new_list.append(x + 1)

print(new_list)
# [2, 3, 4, 5, 6]

上のサンプルにおける for 文のコードブロックでは、リストの各要素に対する操作が行われています。プログラミングに慣れた人なら、たったこれだけのためにループ処理の中で何度も繰り返し関数を呼び出されているのを見るだけで、直感的に実行速度の低下を感じて「うんざり」してしまうかもしれません。しかし、Python にはこの種の処理を簡潔に記述するために リスト内包表記 が備わっています。

# In[2]

# 内包表記を使ってリストの各要素に1を加える
new_list = [x + 1 for x in num_list]

print(new_list)
# [2, 3, 4, 5, 6]

上のコードではリストの各要素に 1 を加えた新しいリストを作る処理を

new_list = [x + 1 for x in num_list]

という1行のコードにまとめています。リスト内包表記の基本形は

[xの処理 for x in イテラブル]

です。x は繰り返し変数なので、i や k など好きな文字を使ってください。もとのイテラブルオブジェクトは必ずしもリスト型である必要はありませんが、戻り値はリストになります。もとのイテラブルオブジェクトから要素を順に取り出して x に入れて、指定した処理を施してから、新しいリストの要素としてゆきます。
 
リストの各要素を 2 乗してすべて足し合わせてみましょう。これも内包表記を用いると簡潔に表現できます。

# In[3]

# 内包表記で平方和を計算
sq = sum([x ** 2 for x in num_list])

print(sq)
# 55

上のコードでは、元のリストの要素をすべて 2 乗した要素をもつ新しいリストを作ってから、sum 関数でそれらの要素をすべて足し合わせることによって、平方和の計算を行なっています。
 
次は内包表記の基本形の後ろに条件式を添えてみます。

[xの処理 for x in イテラブル if x の条件式]

この形式で記述した場合、if のあとに添えられた条件式を満たしている場合に限って、x に処理が施されて新しいリストに追加されます。言い換えると、もとのリストから取り出された要素が条件を満たしていない場合、その要素は破棄されることになります。
 
簡単な例として、元のリストから 100 を超える要素だけを取り出すコードを書いてみます。

# In[4]

# 条件式を含むリスト内包表記[1]

# リスト型データを定義
num_list = [78, 155, 103, 21, 59, 117]

# 内包表記で100を超える要素だけを取り出す
new_list = [x for x in num_list if x > 100]

print(new_list)
# [155, 103, 117]

次はもう少し複雑な条件式です。リストの要素から “p” というアルファベットを含む単語だけを抜き出します。

# In[5]

# 条件式を含むリスト内包表記[2]

# フルーツのリスト
fruits = ["apple", "orange", "strawberry", "pear", "plum"]

# 内包表記で"p"を含む要素だけを取り出す
new_fruits = [x for x in fruits if x.find("p") != -1]

print(new_fruits)
# ['apple', 'pear', 'plum']

条件式のところで find メソッドを用いています。find メソッドは対象とする文字列の中に指定文字がみつからなければ「-1」を返してきます。「!=」は「等しくない」という意味の比較演算子です。つまり「 x が -1 に等しくないならば、それは “p” という文字を含むのだから要素に加えなさい」ということです。

map関数と内包表記

リスト内包表記は他の多くのリスト操作の記法を代替できるうえに、コードはシンプルかつ、ほとんどの場合でより高速です。たとえば、map() 関数の処理は内包表記で置き換えられる最も分かりやすい例の一つです。map() でリストの各要素を 3 倍にするコードは以下のようになります。

# In[6]

# [0, 1, 2, 3, 4]
numbers = range(5)

# map関数で各要素を3倍にする
# mapの戻り値はイテレータなのでlistで展開する
triple_numbers = list(map(lambda x:3*x, numbers))

print(triple_numbers)

# [0, 3, 6, 9, 12]

同じ処理を内包表記で書くと次のようになります。

# In[7]

# 内包表記で各要素を3倍にする
triple_numbers = [x*3 for x in numbers]

print(triple_numbers)

# [0, 3, 6, 9, 12]

map() だと要素を 3 倍にするような簡単な処理でさえ、第1引数に関数形式で渡さなければならないので、少し面倒です。対して、内包表記の場合は普通に算術演算子で記述できます。

内包表記による多重ループ

次は多重ループを考えてみましょう。ドリンクメニューに (コーラ, アイスコーヒー, オレンジ) があって、それぞれに (S、M、L) のいずれかのサイズを選べるとします。このとき、考えられるドリンクの種類とサイズの組み合わせをタプルで列挙してみましょう。その組み合わせは 3×3 = 9 種類あるはずです。for構文を使って素直に書くなら、以下のようなコードになるはずです。

# In[8]

from pprint import pprint

drinks = ['コーラ', 'アイスコーヒー', 'オレンジジュース']
sizes = ['S', 'M', 'L']

my_order = []

for x in drinks:
    for y in sizes:
        my_order.append((x, y))

pprint(my_order)

'''
[('コーラ', 'S'),
 ('コーラ', 'M'),
 ('コーラ', 'L'),
 ('アイスコーヒー', 'S'),
 ('アイスコーヒー', 'M'),
 ('アイスコーヒー', 'L'),
 ('オレンジジュース', 'S'),
 ('オレンジジュース', 'M'),
 ('オレンジジュース', 'L')]
 '''

少し気を利かせて、itertools モジュールの product 関数を活用するなら、もっとスマートなコードを書けます。

# In[9]

from itertools import product

# drinksとsizesの直積集合を生成
my_order = list(product(drinks, sizes))

しかし、わざわざ余計なモジュールを呼び出さなくても、内包表記を使えば、同じことをたった一行のコードで実行できます。

# In[10]
my_order = [(x, y) for x in drinks for y in sizes]

このように、内包表記で多重ループさせるときは、後ろに「for 変数 in イテラブル」を必要な分だけ付加します。

辞書内包表記

キーと値のペアを要素にもつ辞書(ディクショナリ)の内包表記は、リスト内包表記よりも少し複雑です。基本的な書き方は次のようになります。

{xの処理 : y の処理 for x, y in ディクショナリ.items()}

items メソッドを使うことによって、キーと値の両方に対して処理を行なって新しい辞書を作ります。x, y はそれぞれキーと値が入る変数です。
 
書籍の表題をキー、税抜価格を値とするディクショナリを定義して、書籍の表題と税込価格のディクショナリを作ります。この記事を執筆している 2018 年現在の消費税は 8% (2019 年に 10 % となる予定) なので、税抜価格を 1.08 倍にして小数点を捨てれば税込価格が得られます。

# PYTHON_DICTIONARY_COMPREHENSION

# In[1]

# ディクショナリ内包表記[1]

# 書籍と税抜き価格のディクショナリ
# _ex はtax excluded(税抜)を意味する添字
book_ex = {"Pythonスタートブック":2500,\
           "Pythonの絵本":1780,\
           "入門Python3":3700}

# 内包表記で書籍と税込み価格のディクショナリを作成
# _inはtax included(税込み)を意味する添字
book_in = {x:int(y*1.08) for x, y in book_ex.items()}

print(book_in)
# {'Pythonスタートブック': 2700, 'Pythonの絵本': 1922, '入門Python3': 3996}

処理するのは値のほうなので、x には何も手を加えておらず、もとの辞書のキー(税抜価格)のみが書き換えられて新しい辞書が作られています。
 
ディクショナリ内包表記を使うと簡単にキーと値を入れ替えることができます。以下のサンプルコードでは「書籍:発行元(出版社)」の辞書から「発行元(出版社):書籍」の辞書を作ります。

# In[2]

# ディクショナリ内包表記[2]

# 書籍と発行元(出版社)のディクショナリを定義
# _pub は publisher の略
book_pub = {"詳細! Python3入門ノート":"ソーテック社",\
            "かんたんPython":"技術評論社",\
            "スラスラわかるPython":"翔泳社"}

# 内包表記でキーと値を入れ替える
pub_book = {p:b for b,p in book_pub.items()}

pub_book
# {'ソーテック社': '詳細! Python3入門ノート', '技術評論社': 'かんたんPython', '翔泳社': 'スラスラわかるPython'}

内包表記のコード

pub_book = {y:x for x,y in book_pub.items()}

において、x は書籍、y は発行元に対応する変数ですが、for の前の処理文が「y:x」と逆順になっています。つまり、もとの辞書の値をキー、キーを値にするという処理が行われて、キーと値の交換が行われているのです。

set内包表記

set とリストは非常によく似た性質をもつオブジェクトなので、set 内包表記はリスト内包表記とほぼ同じような記述で使えます。

{xの処理 for x in イテラブル if x の条件式}

とはいえ、せっかくですから、set オブジェクトならではの「要素の重複を取り除く」という性質を生かしたサンプルコードを書いてみます。最初文字列を定義して、その中で使われているアルファベットを重複なしに小文字で列挙してみます。

# PYTHON_SET_COMPREHENSION

# In[1]

# set内包表記

# 文字列を定義
my_string = "Learn Python in One Day and Learn It Well"

# 内包表記で文字列を1文字ずつ分割して重複は省く
# ただし空白は除く
my_words = {x.lower() for x in my_string if x != ' '}

my_words
# {'a', 'd', 'e', 'h', 'i', 'l', 'n', 'o', 'p', 'r', 't', 'w', 'y'}

文字列は1文字ずつにインデックスが割り当てられたオブジェクトなので、それを内包表記で1文字ずつ取り出す操作を行なうと、文字列はバラバラにされて、1文字を1要素として set の中に格納されていきます。

内包表記の処理速度

内包表記を応用すると、複数の変数を組み込んで、2 次元や 3 次元の配列をたった1行のコードで生成したりすることもできるようになります。
 
しかし、内包表記はコードをすっきりとまとめるためだけに存在するのではなく、処理速度を大幅に向上させるという利点をもちます。for 構文などを使ってイテラブルオブジェクトの要素を操作する場合には、どうしてもコードブロックで append 関数などを呼び出さなくてはなりません。一方で内包表記はリストの要素を直接操作する記法なので、大きなデータを扱う場合には速度面で圧倒的に有利です。

具体的に、どれくらいの速度差となるのか調べてみましょう。
リストの各要素を 3 倍する処理を、for 構文、map 関数、内包表記で書いてみます。

# In[1]

# 処理速度計測用関数の定義

def test_for(n):
    my_list = []
    for x in range(n):
        my_list.append(x * 3)

def test_map(n):
    list(map(lambda x:3*x, range(n)))

def test_comprehension(n):
    [x*3 for x in range(n)]

PC のスペックに依存しないように、Google Colab で処理時間を計測してみると、以下のようになりました。

# In[2]

%timeit test_for(10**3)
# 110 µs ± 29.6 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

# In[3]

%timeit test_map(10**3)
# 114 µs ± 12.2 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

# In[4]

%timeit test_comprehension(10**3)
# 67.2 µs ± 767 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

for 構文と map 関数にほとんど差はありませんが、リスト内包表記は for 構文に比べておよそ 1.6 倍速いことがわかります。

 

コメント

  1. 小田英世 より:

    分かりやすいサイトで大変助かっております。
    以下のコードを実行すると、エラーがでました。
    「fruit_list」を「fruits」に変えるとうまくいきました。ご確認ください。
    ========================================
    # In[5]

    # 条件式を含むリスト内包表記[2]

    # フルーツのリスト
    fruits = [“apple”, “orange”, “strawberry”, “pear”, “plum”]

    # 内包表記で”p”を含む要素だけを取り出す
    new_fruits = [x for x in fruit_list if x.find(“p”) != -1]

    print(new_fruits)

    • あとりえこばと より:

      申し訳ありません。
      記事は訂正させていただきました。
      大変助かりました。
      ありがとうございます。