Continue(s)

Twitter:@dn0t_ GitHub:@ogrew

内包表記のあれやこれ。

※この記事はQiitaに2017年01月15日に投稿したものと同一です。

qiita.com

Python使ってて内包表記もろくに書けないんじゃだめだろ」

と偉い人に言われたのでいろんなサイト見て、お勉強。

(前提知識は「え、リスト内包だけじゃないんだ!?」レベル)

内包的記法と外延的記法

  • 外延的記法(extensional definition)…具体的に書く
    ex) {1,3,5,7,9}
  • 内包的記法(intensional definition)…性質を書く
    ex) {x|x∈ℕ,x<10,x%2=1}

Wikipedia - 内包と外延

Pythonの内包表記4パターン

1.リスト内包

通常のリスト生成

extension_li = []
for x in range(10):
extension_li.append(x**2)
extension_li

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

 

リスト内包表記

[]で定義する。リストを返す。

comprehension_li = [x**2 for x in range(10)]
comprehension_li

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

2.セット内包

{}で定義する。

comprehension_set = {i/2 for i in range(1,11)}
comprehension_set

{0.5, 1.0, 2.0, 2.5, 1.5, 3.0, 3.5, 4.0, 4.5, 5.0}

3.辞書内包

abb = ("NY","TX","MA","WA","CT")
state = ("New York", "Texas", "Massachusetts", "Washington", "Connecticut")

comprehension_dict = {i:j for i,j in zip(abb,state)}
comprehension_dict

{'WA': 'Washington', 'TX': 'Texas', 'NY': 'New York', 'MA': 'Massachusetts', 'CT': 'Connecticut'}

4.ジェネレータ式

()で定義する。
決してタプル内包表記にはならず、返ってくるのは要素を生成するジェネレータ。リストのようにメモリ中に全要素を格納しない分、メモリの節約になる。

comprehension_gen = (i%3 for i in range(1,10))
comprehension_gen

at 0x014CD900>

for i in comprehension_gen:print(i)

1 2 0 1 2 0 1 2 0

ifの場所

単純にif文を含む場合

extension = [i for i in range(1,10) if i%2==0]
extension

[2, 4, 6, 8]

if~else文となる場合

comprehension = [i if i%2==0 else 0 for i in range(1,10)]
comprehension

[0, 2, 0, 4, 0, 6, 0, 8, 0]

elseが入ると条件表記が先に来る。
理由は明確で、「単純にif文の場合」は通常の内包表記の文法であるのに対して「if~else文の場合」は三項演算子として処理されているため。

ちなみに、python三項演算子

「 (条件がTrueのときの値) if (条件) else (条件がFalseのときの値) 」

という記述になる。(Java,C言語などとは異なるので注意が必要)

散々使い使い古されたネタで恐縮だが、FizzBuzzもif~else文を使えば容易にワンライナーで書ける。

print(["FizzBuzz" if n % 15 == 0 else "Fizz" if n % 3 == 0 else "Buzz" if n % 5 == 0 else n for n in range(1,101)])

あるいはこんな書き方もできる。

print([(i%3==0 and 1 or 0)*"Fizz"+(i%5==0 and 1 or 0)*"Buzz" or i for i in range(1, 101)])

※ @shiracamus さんからのコメントより、次のように真は1、偽は0として演算できるので、下のように書いたほうが見やすいです。(結果はもちろん同じですが)

print([(i%3==0)*"Fizz"+(i%5==0)*"Buzz" or i for i in range(1, 101)])

[1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz', 11, 'Fizz', 13, 14, 'FizzBuzz', 16, 17, 'Fizz', 19, 'Buzz', 'Fizz', 22, 23, 'Fizz', 'Buzz', 26, 'Fizz', 28, 29, 'FizzBuzz', 31, 32, 'Fizz', 34, 'Buzz', 'Fizz', 37, 38, 'Fizz', 'Buzz', 41, 'Fizz', 43, 44, 'FizzBuzz', 46, 47, 'Fizz', 49, 'Buzz', 'Fizz', 52, 53, 'Fizz', 'Buzz', 56, 'Fizz', 58, 59, 'FizzBuzz', 61, 62, 'Fizz', 64, 'Buzz', 'Fizz', 67, 68, 'Fizz', 'Buzz', 71, 'Fizz', 73, 74, 'FizzBuzz', 76, 77, 'Fizz', 79, 'Buzz', 'Fizz', 82, 83, 'Fizz', 'Buzz', 86, 'Fizz', 88, 89, 'FizzBuzz', 91, 92, 'Fizz', 94, 'Buzz', 'Fizz', 97, 98, 'Fizz', 'Buzz']

mapやfilterで代用できない?

答え
できる。

ここでは例として[1,2,3,4,5,6,7,8,9,10]の各値を二乗したリストを作る操作を比較する。

%%timeit
list(map(lambda i : i**2, range(1,11)))

10000 loops, best of 3: 23.8 µs per loop

%%timeit
[i**2 for i in range(1,11)]

100000 loops, best of 3: 15.8 µs per loop

filterに関しては[1,2,3,4,5,6,7,8,9,10]から偶数を抜く操作を比較する。

%%timeit
list(filter(lambda i : i%2==0, range(1,11)))

10000 loops, best of 3: 19.3 µs per loop

%%timeit
[i for i in range(1,11) if i%2==0]

100000 loops, best of 3: 10.2 µs per loop

見てもらって分かるように速度が違う。可読性も(何をしているのかすぐわかるという意味では)内包表記のほうに軍配が上がる(個人差)。

速度の話

処理速度の話が出たのでついでに。 最初に出たリスト内包表記と通常のリスト生成では速度を比較してみるとどうなるか。

def for_loop(n):
extension = []
for x in range(n):
extension.append(x**2)
return extension
def comprehension(n):
return [x**2 for x in range(n)]
%timeit for_loop(1000)

1000 loops, best of 3: 1.58 ms per loop

%timeit comprehension(1000)

1000 loops, best of 3: 1.16 ms per loop

僕の低スぺPCだとあまり差は実感しづらいが、それでも違いは明確。 理由は簡単に言ってしまえば前者はfor文を回すたびにappend属性の取り出しに時間がかかっているため。 物は試しで、append属性の参照だけfor文の外に出してみて時間を測る。

def for_loop2(n):
extension = []
app = extension.append
for x in range(n):
app(x**2)
return extension
%timeit for_loop2(1000)

1000 loops, best of 3: 1.27 ms per loop

予想通り処理時間は、for_loop>for_loop2>comprehensionの順になった。 for_loop2とcomprehensionの差はつまりはappend自体の速さを意味しているのだろう。

さいごに

なんとなくではあるが内包表記の扱い方がわかった。

これで今日からPythonマスター…とは行くわけもなく…。

というかこの内包表記python特有の書き方なのかなと思っていたがほかの多くの関数型言語では普通に使うものらしい。

それら含めて参考とさせていただいたサイトを載せておく。

~参考~

DSAS開発者の部屋-Pythonの内包表記はなぜ速い?

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

stackoverflow - if else in a list comprehension

知に至る病 - Python の内包表記で if ~ else を使った場合分けをする

Life with Python - Pythonの内包表記の使い方まとめ