pythonの内包表記について
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'