内包表記で遊んでたらいろいろと思うところがあったので、勉強ついでにまとめていきたいと思います
内包表記とは
コンテナオブジェクト等がイケイケに書けるPython独自の記法
リスト操作をなかなか速く実装できるので、Pythonista必須能力
諸注意
- 対話モードでの実行を想定
- Python3.7.4
- 出力結果は、見やすいように加工される場合があります(内容に変化はないです)
まずは基本から
内包表記の基本
[要素の処理 for 要素 in コンテナ]
list、range、str、dict、といったコンテナオブジェクトから
要素を1つずつ取り出し、処理を加えることができます
例:0から9までのリスト
l = []
for i in range(10) :
l.append(i)
[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))
速度比較してみます
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さん、ありがとうございます!)
[x for x in range(2,100) if all(x % y or x == y for y in [2,3,5,7])]
**オマケ**
>>> 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ったほうが篩っぽいです
{
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 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の引数はできるだけジェネレータ式などで書く