Python

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

More than 1 year has passed since last update.


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'