ラベル comprehensions の投稿を表示しています。 すべての投稿を表示
ラベル comprehensions の投稿を表示しています。 すべての投稿を表示

2015年12月2日

内包表記の話3

内包表記の可読性について考えてみます。


一般に内包表記は for文で書くよりも短い記述で済み、また動作速度も速いです。
これらは非常に大きな恩恵なのですが、他方で可読性について考慮すると、いつでも内包表記の方が良いとは限りません。

元々内包表記は、
[値 繰り返し 条件文]
というやや特殊な並びの記述です。慣れの問題が大きいのですが、Python ベッタリではない人にとってはこの表記はなかなか馴染めないようです。

それよりも、内包表記が入れ子になっていたり、記述があまりにも長い場合には、可読性の観点から内包表記を断念した方が良いようになります。
以下は読みにくい内包表記の例です。

# 九九のリストを作成。
# 内包表記が入れ子なので読みにくい
>>> qq = [[i * j for i in range(1, 10)] for j in range(1, 10)]
>>> qq
[[1, 2, 3, 4, 5, 6, 7, 8, 9],
 [2, 4, 6, 8, 10, 12, 14, 16, 18],
 ...
 [9, 18, 27, 36, 45, 54, 63, 72, 81]]

# 条件に合うファイル名を取得。
# 複数の処理が詰め込まれていて読みにくい
>>> filelist = ['img/a.jpg', 'img/b.jpeg', 'img/c.png']
>>> [os.path.basename(f) for f in filelist if os.path.isfile(f) and f.endswith(('.jpg', '.jpeg'))]
['a.jpg', 'b.jpeg']


以下の2つの条件を満たしている時のみ内包表記を書くべきだと私は考えています。あくまで私見ですが、、、。
  • 入れ子になっていないこと
  • 1行で書き切れること

2015年12月1日

内包表記の話2

リスト内包表記とジェネレーター内包表記を動作速度の観点から比較します。


以下は sum([i for i in range(10000)]) と sum(i for i in range(10000)) の動作速度を比較するベンチマークです。

from benchmarker import Benchmarker

with Benchmarker(1000, cycle=5, extra=1) as bench:
    sample = range(10000)
    @bench("list")
    def _(bm):
        for _ in bm:
            sum([i for i in sample])

    @bench("generator")
    def _(bm):
        for _ in bm:
            sum(i for i in sample)

ベンチマーク結果です。
## Ranking      real
list          2.4163  (100.0) ********************
generator     3.1262  ( 77.3) ***************
結果からは、リスト内包表記の方が高速、と言えます。
この結果だけを見ると、ジェネレーター内包表記を使う理由は見当たりません。




次に、range の値を 10000000 と先例よりもずっと大きくしてみます。

from benchmarker import Benchmarker

with Benchmarker(5, cycle=5, extra=1) as bench:
    sample = range(10000000)
    @bench("list")
    def _(bm):
        for _ in bm:
            sum([i for i in sample])

    @bench("generator")
    def _(bm):
        for _ in bm:
            sum(i for i in sample)

結果です。
## Ranking      real
generator    26.6801  (100.0) ********************
list         27.8582  ( 95.8) *******************
今度はジェネレータ内包表記の方が高速になりました。


さらに、ベンチマーク実行中のメモリ使用量を計測してみます。

# リスト内包表記動作中
C:\>tasklist /fi "imagename eq python.exe"
イメージ名    PID セッション名  メモリ使用量
=========== ===== ============= ============
python.exe   2772 Console          210,660 K

# ジェネレータ内包表記動作中
C:\>tasklist /fi "imagename eq python.exe"
イメージ名    PID セッション名  メモリ使用量
=========== ===== ============= ============
python.exe   2772 Console           14,704 K

リスト内包表記は、200MB を超えるメモリ使用量となっています。サイズが 10000000 のリストを作成するため、メモリ上に 10000000個の int を確保しているので、メモリ使用量が増大していると思われます(と同時に、処理速度が遅い原因になっている)。
他方、ジェネレータ内包表記では、メモリ使用量は極々平常です。
メモリ使用量の比較においては、ジェネレータが遅延評価で動作することの利点が強く出ています。





結果を振り返ります。
forループの回数が小さければ、リスト内包表記の方がジェネレータ内包表記よりも高速でした。
forループの回数が大きくなると、ジェネレータ内包表記の方が高速となり、かつメモリ使用量が少なくて済みました。

この結果を踏まえ、forループのサイズによってリスト内包表記かジェネレータ内包表記かを切り替える、という手法が考えられます。
が、個人的には、この手法は早過ぎる最適化になるのではないかと思います。どこまでが小さいデータでどこからが大きなデータなのか、というのは環境に依存するので一概には言えませんし、速度に気を取られてメモリ不足が起こってしまっては元も子もありません。

「ジェネレータ内包表記で書ける場面では常にジェネレータ内包表記を使う」という方式の方が、ルールとしてシンプルで良いのではないかと思います(もちろん、処理速度が問題になれば適宜対応が必要となります)。

2015年11月30日

内包表記の話1

Python 文法の特徴の一つと言える内包表記について話します。
本項では内包表記の種類についてまとめます。


一口に内包表記と言っても、その種類は4つあります。

  • リスト内包表記
  • dict内包表記
  • set内包表記
  • ジェネレーター内包表記
それぞれの定義方法は以下のようになります。
>>> data = [1, 4, 2, 8, 5, 7]

# リスト
>>> l = [i for i in data]

# dict
>>> d = {i:str(i) for i in data}

# set
>>> s = {i for i in data}

# ジェネレータ
>>> g = (i for i in data)


この中で最も有名なのがリスト内包表記でしょう。Python に最初に実装された内包表記がリスト内包表記ですし、単に内包表記と言えばそれがリスト内包表記のことを指すことも多いです(この言葉の使い方は正確ではないのですが)。

dict内包表記と set内包表記の使い方は、リスト内包表記と同等です。出力が dict か set である点のみが違いです。

ジェネレーター内包表記は出力がジェネレーターであるため、使い所が他の3つとは異なります。
ジェネレーターから出力される値そのものはリスト内包表記のリストと同じです。では、どのような場面でジェネレーター内包表記を使うべきなのか、次項にて紹介します。

2015年11月18日

ベンチマーク6 リスト内包表記

リストを作成する操作について、ベンチマークを計測します。


リストを作成する操作として、以下の2つを比較します。1番目は空のリストを作ってから append していく方法、2番目はリスト内包表記、となります。

  1. list.append へのショートカットを利用
  2. リスト内包表記

ベンチマークのソースコードです。
from benchmarker import Benchmarker

with Benchmarker(1000, cycle=3, extra=1) as bench:
    sample = range(10000)

    @bench("append")
    def _(bm):
        for _ in bm:
            l = list()
            append = l.append
            for i in sample:
                append(i)

    @bench("comprehensions")
    def _(bm):
        for _ in bm:
            l = [i for i in sample]
計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking                real
comprehensions          2.5492  (100.0) ********************
append                  3.2118  ( 79.4) ****************

計測結果より、リスト内包表記の方が高速であると言えます。
「list.append へのショートカットを利用」はこちらでは最速の手法でしたが、リスト内包表記はそれよりも高速でした。

さらに、リスト内包表記は記述が1行で済んでおり、高速さと簡潔さを兼ね備えた実装方法であると言えます。内包表記を使える場面では迷わず使っていきましょう。