1082
1082

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-03-14

pythonの内包表記について

[nbviewer]
(http://nbviewer.jupyter.org/format/slides/github/y-sama/comprehension/blob/master/comprehension.ipynb#/)にも投稿済。

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'
1082
1082
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1082
1082

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?