pythonの内包表記を少し詳しく

  • 269
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

pythonの内包表記について

nbviewerにも投稿済。

pythonといえば内包表記です。 <= 偏見?
でも、慣れないと読みにくいので、少し詳しく読み方をまとめてみました。

通常のリスト生成

extension_1 = []
for i in range(10):
    extension_1.append(i)
extension_1
#>>> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

リスト内包表記

基本構文
[counter for counter in iterator]
僕は先に [i for i in] だけ書いてから修飾することが多いです。

extension_1と同等のリストを内包表記で生成する場合は

comprehension_1= [i for i in range(10)]
comprehension_1
#>>> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

速度

リスト内包表記を使うかどうかの実行速度を比べてみます。
jupyterのセルマジック%%timeitを用いて測定しました。

%%timeit
extension_1 = []
for i in range(10000):
    extension_1.append(i)
#>>> 100 loops, best of 3: 3.37 ms per loop
%%timeit
comprehension_1= [i for i in range(10000)]
#>>> 1000 loops, best of 3: 1.05 ms per loop

リスト内包表記はコードがすっきりするだけでなく速度面でも有利です
参考: Pythonの内包表記はなぜ速い?
遅い理由は大きく2つあります。

  • ループする度にリストオブジェクトのappendを参照する
  • appendをpythonの関数として実行する

前者の影響はループの外に参照を追い出すことでも解消できます。

%%timeit
extension_1_ = []
append=extension_1_.append
for i in range(10000):
    append(i)
#>>> 1000 loops, best of 3: 1.57 ms per loop

一般にリスト内包表記にすると実行速度が2倍になると言われていますが、その8割くらいはappendメソッドの呼び出し部分のオーバーヘッドによるものです。

ifを含む場合(後置if)

pythonには後置if文がありませんが、リスト内包表記に限っては(結果的にですが)書けます。

extension_2 =[]
for i in range(10):
    if i%2==0:
        extension_2.append(i)
extension_2
#>>> [0, 2, 4, 6, 8]

extension_2をリスト内包表記で書きなおすと下記のような感じです。

comprehension_2 = [i for i in range(10) if i%2==0]
comprehension_2
#>>> [0, 2, 4, 6, 8]

結果的に後置ifの構文になっていますが、これは内包表記ではfor節の後にif節やfor節がつなげられるためです。(コロンとインデントを省略できると思えばよい。)

インデントつけると下記のようなイメージです。

[
i
for i in range(10)
  if i%2==0
]

闇編でもう少し詳しく説明します。

%%timeit
extension_2 =[]
for i in range(10000):
    if i%2==0:
        extension_2.append(i)
#>>> 100 loops, best of 3: 4.08 ms per loop
%%timeit
comprehension_2 = [i for i in range(10000) if i%2==0]
#>>> 100 loops, best of 3: 3.15 ms per loop
%%timeit
extension_2_ =[]
append=extension_2_.append
for i in range(10000):
    if i%2==0:
        append(i)
#>>> 100 loops, best of 3: 3.81 ms per loop

実はifが計算律速なので、無理にリスト内包表記にしても20-30%くらいしか速度が改善しません。

if ~ elseを含む場合 (条件演算子)

紛らわしいですが、else節を含む場合は条件演算子(他で言う三項演算子)を使うのでifの位置が変わります
(条件演算子はpython 2.5以降のみ対応です)

extension_3 =[]
for i in range(10):
    if i%2==0:
        extension_3.append(i)
    else:
        extension_3.append(str(i))
extension_3
#>>> [0, '1', 2, '3', 4, '5', 6, '7', 8, '9']
comprehension_3 = [ i if i%2==0 else str(i) for i in range(10)]
comprehension_3
# >>> [0, '1', 2, '3', 4, '5', 6, '7', 8, '9']

実際にはこちらと等価だと思えば理解しやすいかもしれません

extension_3_cond =[]
for i in range(10):
    extension_3_cond.append(i) if i%2==0 else extension_3_cond.append(str(i))
extension_3_cond
#>>> [0, '1', 2, '3', 4, '5', 6, '7', 8, '9']

後置ifの書き方に引っ張られて、if~elseを後につけるとエラーが出ます。

# 動きません
[ i for i in range(10) if i%2==0 else str(i)]
#>>> SyntaxError: invalid syntax

if~elseが律速になるので、あまり内包表記による高速化はありません。

辞書内包表記とセット内包表記

python2.7以降ではリスト以外の内包表記として、辞書内包やセット内包も使えます。

comprehension_dict = {str(i):i for i in range(10)}
print(comprehension_dict)
#>>> {'7': 7, '8': 8, '2': 2, '9': 9, '0': 0, '1': 1, '6': 6, '5': 5, '4': 4, '3': 3}

zipとかと相性いいです。

label = ["kinoko", "takenoko", "suginoko"]
feature = ["yama", "sato", "mura"]
{i:j for i,j in zip(label,feature)}
#>>> {'kinoko': 'yama', 'suginoko': 'mura', 'takenoko': 'sato'}

このケースのように単純にkeyとvalueを渡すだけであれば、内包表記を使う必要はありませんが。
dict(zip(label,feature))

python2.6まではdictにtupleを渡してあげます。

comprehension_dict2 = dict((str(i),i) for i in range(10))
print(comprehension_dict2)

後置ifも使えます。

comprehension_dict2 = {str(i):i for i in range(10) if i%2==0}
print(comprehension_dict2)
#>>> {'8': 8, '6': 6, '2': 2, '4': 4, '0': 0}

条件演算子も使えます。
条件演算子なので、「:」の前後のkeyとvalueそれぞれに記載する必要があります。

comprehension_dict3 = {str(i) if i%2==0 else i : i if i%2==0 else str(i) for i in range(10)}
print(comprehension_dict3)
#>>> {'2': 2, 1: '1', 3: '3', 5: '5', '0': 0, 7: '7', 9: '9', '6': 6, '4': 4, '8': 8}

これは動かないです。(以前やりましたorz)

#動きません
comprehension_dict4 = {str(i):i if i%2==0 else i:str(i) for i in range(10)}
#>>> SyntaxError: invalid syntax

セット内包表記

コロンなしで {} で囲めばセット内包表記になります。

comprehension_set={i%5 for i in range(10)}
comprehension_set
#>>> {0, 1, 2, 3, 4}

要素ゼロの{}は辞書を意味するので注意。

zero_set={}
type(zero_set)
# >>> dict

ジェネレータ式とタプル内包表記

構文から勘違いしやすいですが、()で囲んでもタプル内包表記にならずにジェネレータ式になります。

comprehension_gen=(i%5 for i in range(10))
comprehension_gen
#>>> <generator object <genexpr> at 0x7f3000219678>

for i in comprehension_gen:print(i)
#>>> 0
#>>> 1
#>>> 2
#>>> 3
#>>> 4
#>>> 0
#>>> 1
#>>> 2
#>>> 3
#>>> 4

むしろタプル内包表記より使います。
リストと違ってメモリ中に全要素を格納しないで、次の要素を順番に生成します。
内包表記を使わない書き方は下記ですが、一旦 ジェネレータを生成する関数を作らないと行けないので面倒です。

def gen_func():
    for i in range(10):
        yield i%5
extension_gen = gen_func()
extension_gen
#>>> <generator object gen_func at 0x7f3000166830>

タプル内包表記が必要なことはあまりないですが、もしどうしても必要ならリスト内包表記をtuple関数に渡せば作れます。

tuple([i for i in range(10)])
#>>> (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

[追記]
コメントを頂いた内容を追記します。
渡すのはジェネレータでもよく、ジェネレータはかっこを省略できるので可読性に優れると思います。(タプル内包表記っぽい書き方になりますし。)

tuple(i for i in range(10))
#>>> (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

# 以下の構文のカッコの省略形
tuple((i for i in range(10)))

関数型プログラミングとの比較

map,filterと内包表記はアウトプットが近く、よく比較されます。
また、python 2系ではmap,filterはリストを返すため、リスト内包表記と対応します。
python 3系ではmap,filterオブジェクトというイテレータを返すため、ジェネレータと対応します。

mapでの書き換え

map(lambda i:i**2,range(1,11))
# python2.xの場合
#>>> [1, 4, 9, 16, 25, 36, 49, 64, 100]
# python3.xの場合は返ってくるのはマップオブジェクトという名前のイテレータ
#>>> <map at 0x4570f98>

list(map(lambda i:i**2,range(1,11)))
#>>> [1, 4, 9, 16, 25, 36, 49, 64, 100]

#リスト内包表記だと
[i**2 for i in range(1,11)]
#>>> [1, 4, 9, 16, 25, 36, 49, 64, 100]

filterでの書き換え

filter(lambda i:i%2==1, range(1,11))
# python2.xの場合
#>>> [1, 3, 5, 7, 9]
# python3.xの場合は返ってくるのはフィルターオブジェクトという名前のイテレータ
#>>> <filter at 0x4578a20>

list(filter(lambda i:i%2==1, range(1,11)))
#>>> [1, 3, 5, 7, 9]
#リスト内包表記だと
[i for i in range(1,11) if i%2==1]
#>>> [1, 3, 5, 7, 9]

ネストすると

map(lambda j:j**2, filter(lambda i:i%2==1, range(1,11)))
# python2.xの場合
#>>> [1, 9, 25, 49, 81]
# python3.xの場合は返ってくるのはフィルターオブジェクト
#>>> <filter at 0x4578a20>

list(map(lambda j:j**2, filter(lambda i:i%2==1, range(1,11))))
#>>> [1, 9, 25, 49, 81]

#リスト内包表記だと
[i**2 for i in range(1,11) if i%2==1]
#>>> [1, 9, 25, 49, 81]

僕はリスト内包表記の方が読みやすいんですが、慣れの問題ですかね?
また、一般的にはリスト内包表記の方が高速です。

闇への道

この節を読み終えた後はリーダブルコードを読んで心を浄化することを推奨します。
基本的な内包表記以外の多段や条件分岐などは使わない方がpythonicな気がします。

条件演算子を複数つなげる

条件演算子を複数つなげる
fizzbuzzとかを例にするとこんな感じ。

fizzbuzz=[]
for i in range(1,16):
    if i%15==0:
        fizzbuzz.append("fizzbuzz")
    elif i%3==0:
        fizzbuzz.append("fizz")
    elif i%5==0:
        fizzbuzz.append("buzz")
    else:
        fizzbuzz.append(i)
#>>> [1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz']
["fizzbuzz" if i%15==0 else "fizz" if i%3==0 else "buzz"
 if i%5==0 else i for i in range(1,16)]
#>>> [1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz']

ネスト(多重配列)

配列をネストしたいケースは結構あると思います。
リスト内包表記にリスト内包表記をネストすればできます。

outer_list=[]
for i in range(3):
    innter_list=[]
    for j in range(10):
        innter_list.append(j)
    outer_list.append(innter_list)
outer_list
#>>> [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
#>>>  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
#>>>  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]

# リスト内包表記で書くと
[[j for j in range(10)] for i in range(3)]
#>>> [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
#>>>  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
#>>>  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]]

三重あたりになるとpythonなのに読みにくくなります。

二重ループ

flattenを例に取ります。
一度理解すればそれほど読みにくくないのですが、
初見だと可読性が著しく悪くなります。

init=[[1,2,3],[4,5],[6,7]]
flatten=[]
for outer in init:
    for inner in outer:
        flatten.append(inner)
flatten
#>>> [1, 2, 3, 4, 5, 6, 7]

# これと等価
[inner for outer in init for inner in outer]
#>>> [1, 2, 3, 4, 5, 6, 7]

基本的には左側のfor節から順番に読んでいって、最後にリストに入れるものが頭に来ます。
インデントするならこんな感じです。

[
inner
for outer in init
    for inner in outer
]
#>>> [1, 2, 3, 4, 5, 6, 7]

ifを含む場合(後置if)節で示したのと同様に、for節毎にコロンとインデントを省略したような挙動を示します。

パタトクカシー

zipと二重ループを組み合わせればワンライナーでパタトクカシーを生成できます。

patato=[]
for i in zip("パトカー","タクシー"):
    for j in i:
        patato.append("".join(j))
"".join(patato)
#>>> 'パタトクカシーー'

# 内包表記で書くと
"".join(["".join(j) for i in zip("パトカー","タクシー") for j in i])
#>>> 'パタトクカシーー'

# インデントすると
"".join(
  [
  "".join(j)
  for i in zip("パトカー","タクシー")
    for j in i
  ]
)
#>>> 'パタトクカシーー'

printとか入れてもできます。

[print(k) for i in zip("パトカー","タクシー") for j in i for k in j]
#> パ
#> タ
#> ト
#> ク
#> カ
#> シ
#> ー
#> ー
#>>> [None, None, None, None, None, None, None, None]
# print関数の返り値はないので、Noneの詰め合わせになります。

if 節との多重ループの合わせ技

なかなかです。
これに更に多重配列とかlambdaとか入れるといい感じの可読性になります。

import re
DIO=["U","無駄","RR","貧弱ゥ","Y"]

rslt=[]
for i in DIO:
    if re.match("[URY]+",i):
        for j in i:
            rslt.append(j*2)
"".join(rslt)
#>>> 'UURRRRYY'

# 内包表記
"".join([j*3 for i in DIO if re.match("[URY]+",i) for j in i])
#>>> 'UUURRRRRRYYY'

# インデントするとこんな感じ
"".join(
  [
    j*4
    for i in DIO
      if re.match("[URY]+",i)
        for j in i
  ]
)
#>>> 'UUUURRRRRRRRYYYY'