LoginSignup
8
4

More than 3 years have passed since last update.

Python内包表記のまとめ(そして闇堕ち)

Last updated at Posted at 2019-08-06

内包表記で遊んでたらいろいろと思うところがあったので、勉強ついでにまとめていきたいと思います

内包表記とは

コンテナオブジェクト等がイケイケに書けるPython独自の記法
リスト操作をなかなか速く実装できるので、Pythonista必須能力

諸注意

  • 対話モードでの実行を想定
  • Python3.7.4
  • 出力結果は、見やすいように加工される場合があります(内容に変化はないです)

まずは基本から

内包表記の基本

基本の形
[要素の処理 for 要素 in コンテナ]

list、range、str、dict、といったコンテナオブジェクトから
要素を1つずつ取り出し、処理を加えることができます

例:0から9までのリスト

従来手法
l = []
for i in range(10) :
    l.append(i)
内包表記で普通にrange
[i for i in range(10)]
結果
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

要素の処理部分に任意の値を入れれば、その値で初期化できます
例:0で初期化

従来手法
l = []
for __ in range(10) :
    l.append(0)
内包表記
[0 for __ in range(10)]
結果
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

追記:@shiracamusさんより

もっと簡潔で速い
[0] * 10

値をいじるわけではなく、初期化だけするのであればこちらの方が速いです!

落とし穴にご注意を

ただし、以下の記述は行わないように
良くない二次元リストの生成
[ [0] * 10 ] * 10

一見二次元ぽいですが、参照先が同じ一次元リストを10個並べてるだけになります、注意です。

map & lambda(内包処理)

基本で述べましたが、コンテナから取り出した値を処理できます
→ mapとlambda組み合わせたやつと同じやん!
例:平方数リスト

従来手法
list(map(lambda x : x ** 2, range(1,11)))
内包表記で平方数のリスト
[i**2 for i in range(1,11)]
結果
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

どちらもワンライナーですが、内包表記の方がスッキリしてますね

filter(後置if)

内包表記内の後ろにifを書くと、リストに格納する要素が減らせます
→ filterとlambda組み合わせたやつと同じやん!
例:偶数だけを取り出す

従来手法
list(filter(lambda x:x % 2 == 0, range(20)))
内包表記
[i for i in range(20) if i % 2 == 0]
結果
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

後置ifは連続で記述することができません
条件分岐などは前置ifもとい三項演算子を使うことで実装できます

前置if(三項演算子, if-else)

内包表記の前にif(三項演算子)を書くと、後置ifと違い要素数は変わりません
また、elseは省略できないことに注意してください
例:3の倍数だけ世界のナベアツ

従来手法
l = []
for i in range(1,20) :
    if i % 3 == 0 :
        l.append("ナベアツ")
    else :
        l.append(i)
内包表記
["ナベアツ" if i % 3 == 0 else i for i in range(1,20)]
結果
[1, 2, 'ナベアツ', 4, 5, 'ナベアツ', 7, 8, 'ナベアツ', 10,
 11, 'ナベアツ', 13, 14, 'ナベアツ', 16, 17, 'ナベアツ', 19]

前置ifネスト

三項演算子を書きまくればif-elif-elseも内包表記で書けます
例:鉄板のFizzBuzz

従来手法
l = []
for i in range(1,20) :
    if i % 15 == 0:
        l.append('FizzBuzz')
    elif i % 3 == 0:
        l.append('Fizz')
    elif i % 5 == 0:
        l.append('Buzz')
    else :
        l.append(i)
内包表記
[
    'FizzBuzz' if i % 15 == 0 
    else 'Fizz' if i % 3 == 0
    else 'Buzz' if i % 5 == 0
    else i
    for i in range(1,20)
]

ワンライン
['FizzBuzz' if i % 15 == 0 else 'Fizz' if i % 3 == 0 else 'Buzz' if i % 5 == 0 else i for i in range(1,20)]

結果
[1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz',
 11, 'Fizz', 13, 14, 'FizzBuzz', 16, 17, 'Fizz', 19]

三項演算子がifの前にTrueのときに格納する値を記述することに注意してください
可読性からして闇が見えてきましたね

多次元内包表記

内包表記の中に内包表記を記述することで、多次元に内包表記が書けます
例:二次元九九リスト

従来手法
l = []
for i in range(1,10) :
    m = []
    for j in range(1,10) :
        m.append(i*j)
    l.append(m)
内包表記
[[i*j for j in range(1,10)] for i in range(1,10)]
結果
[[1,  2,  3,  4,  5,  6,  7,  8,  9],
 [2,  4,  6,  8, 10, 12, 14, 16, 18],
 [3,  6,  9, 12, 15, 18, 21, 24, 27],
 [4,  8, 12, 16, 20, 24, 28, 32, 36],
 [5, 10, 15, 20, 25, 30, 35, 40, 45],
 [6, 12, 18, 24, 30, 36, 42, 48, 54],
 [7, 14, 21, 28, 35, 42, 49, 56, 63],
 [8, 16, 24, 32, 40, 48, 56, 64, 72],
 [9, 18, 27, 36, 45, 54, 63, 72, 81]]

辞書内包表記

dictでも内包表記が使えます ここから可読性がえぐいことになるので注意してください
例:ブラックジャックの得点リスト

従来手法
l = {}
for suit in "♤♧♡♢" :
    for num in ["A",2,3,4,5,6,7,8,9,10,"J","Q","K"] :
        if num in list(range(2,11)) :
            l.update({ suit+str(num) : num })
        elif num in "JQK" :
            l.update({ suit+str(num) : 10 })
        else :
            l.update({ suit+str(num) : [1,11] })
内包表記
{
    suit + str(num) :
        num if num in list(range(2,11))
        else 10 if num in "JQK"
        else [1,11]
    for suit in "♤♧♡♢"
        for num in ["A",2,3,4,5,6,7,8,9,10,"J","Q","K"]
}

ワンライン
{suit + str(num) : num if num in list(range(2,11)) else 10 if num in "JQK" else [1,11]  for suit in "♤♧♡♢" for num in ["A",2,3,4,5,6,7,8,9,10,"J","Q","K"]}

結果(省略)
{'♤A': [1, 11], '♤2': 2, '♤3': 3, '♤4': 4, '♤5': 5, '♤6': 6, 
 '♤7': 7, '♤8': 8, '♤9': 9, '♤10': 10, '♤J': 10, '♤Q': 10, '♤K': 10, ...

辞書のvalueがややこしいことになってますが、
2~10なら数字が得点、JQKのどれかであれば10点、エースの場合は1か11点
をそこで表現しています

セット内包表記

setも使えます、
例:エラトステネスの篩

内包表記(と集合演算)
{i for i in range(2,101)} - {i*j for i in [2,3,5,7,11] for j in range(2,100//i+1)}

ジェネレータ式

もちろん、ジェネレータも生成できます
ジェネレータを内包表記チックに書く場合は、ジェネレータ式と言うそうです
@shiracamusさん、ありがとうございます!)

とはいえ、使い所なんてあるのかなあと調べていたのですが、
【python】ジェネレータ式の使い所
【python】組み込み関数all・anyの引数はできるだけジェネレータ式などで書く
確かに、allはFalseが出現したら、anyはTrueが出現したら処理を打ち切った方が良いですよね!

例:イテレータの値をallで走査

上記参照先、はやたかさんの例を参考にしました

リスト内包表記
all([x < 100 for x in range(10**8)])
ジェネレータ式
all(x < 100 for x in range(10**8))

速度比較してみます

allでリストとジェネレータの速度比較
timeit.timeit(lambda : all([x < 100 for x in range(10**4)]), number=10**3)
0.7012882420001461
timeit.timeit(lambda : all((x < 100 for x in range(10**4))), number=10**3)
0.03139967999982218

段違いですね…
これを利用してエラトステネスの篩がいい感じに書けるようです
@masaruさん、ありがとうございます!)

allを利用してエラトステネスの篩
[x for x in range(2,100) if all(x % y or x == y for y in [2,3,5,7])]

オマケ
anyでリストとジェネレータの速度比較
>>> timeit.timeit(lambda : any([x < 100 for x in range(10**4)]), number=10**3)
0.7887304820001191
>>> timeit.timeit(lambda : any((x < 100 for x in range(10**4))), number=10**3)
0.0024835490000896243

そらそうですよ


lambda

先のエラトステネスの篩
集合演算使わずlambdaったほうが篩っぽいです

lambdaでエラトステネスの篩
{
    x for x in range(2,101)
    if all([
        (lambda x,y : x % y or x is y)(x,y) for y in [2,3,5,7,11]
    ])
}

ワンライン
{x for x in range(2,101) if all([(lambda x,y : x % y or x is y)(x,y) for y in [2,3,5,7,11]])}


lambda式で2,3,5,7,11の倍数を削ってます(2,3,5,7,11で割り切れなければ素数として通す)

闇Python

堕ちました

力が、欲しいか、

内包表記について調べてると、こんな記事を見つけました
リスト内包表記で始める超"実用的"なPythonワンライナー入門
for for ∞ for python

フィボナッチ数列を再帰lambdaからジェネレータ式へ

フィボナッチをlambdaで再帰させて書いてみました
数列のN項目を求められます

lambda式でフィボナッチ
(lambda f: f(N,f))(lambda n,f: f(n-1,f) + f(n-2,f) if n>2 else 1)

そしてジェネレータ版

ジェネレータ式でフィボナッチ
fib =
( ns["a"]
    for ns in [
        ns for ns in [dict()] if not(
            ns.update({"a":0}) or
            ns.update({"b":1})
        )
    ]
    for l in [[None]]
    for __ in l if not (
        ns.update({"c" : ns["a"] + ns["b"]}) or
        ns.update({"a" : ns["b"]}) or
        ns.update({"b" : ns["c"]}) or
        l.append(None)
    )
)

ワンライン
fib = ( ns["a"] for ns in [ ns for ns in [dict()] if not( ns.update({"a":0}) or ns.update({"b":1}))] for l in [[None]] for __ in l if not ( ns.update({"c" : ns["a"] + ns["b"]}) or ns.update({"a" : ns["b"]}) or ns.update({"b" : ns["c"]}) or l.append(None) or None ) )


dict()で定義した変数が取れる&変数が定義できるなんて、目から鱗でした。。。

ワンライン素数炙り出しジェネレータ

ジェネレータ式で素数取り出し
prime =
( x for ns in [
        ns for ns in [dict()] if not(
            ns.update({"era":[]})
        )
    ]
    for x in (len(il)
        for il in [[None]]
            for _ in il if not
                il.append(None)
    )
    if all(x % p > 0 for p in ns["era"]) and
    not ns["era"].append(x)
)

ワンライン
prime = ( x for ns in [ns for ns in [dict()] if not(ns.update({"era":[]}))] for x in (len(il) for il in [[None]] for _ in il if not il.append(None)) if all(x % p > 0 for p in ns["era"]) and not ns["era"].append(x) )


@masaruさんのエラトステネスの篩と、@KTakahiro1729さんの無限イテレータ(itertools.countと同じ)を基に、素数を出力するジェネレータを作ってみました。
ジェネレータ式でフィボナッチ数列を書いたときに、あまりfor ns in [ns for ns in [dict()]...の部分を理解せずに書いていたため、今回の実装では苦労しました…

初めのforでera = []を定義し、次のforで無限イテレータを生成し、最後のifでエラトステネスの篩と篩に素数を追加する処理を行っています。

終わりに

Pythonの内包表記は、スマートに書けて処理速度も早いとめちゃくちゃ美味しい機能だからこそ、使い所を考えていく必要がありますね。
とりあえず、これからはより深く闇に落ちていこうかなと思います

参照先

Guide to Python
リスト内包表記で始める超"実用的"なPythonワンライナー入門
for for ∞ for python
【python】ジェネレータ式の使い所
【python】組み込み関数all・anyの引数はできるだけジェネレータ式などで書く

8
4
11

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
8
4