この本、エキスパートじゃないと理解できない仕様みたいです。これを脚注に入れた稲田さん(@methane)という凄い人が、その内容を以下の記事で解説している。DSAS開発者の部屋:Pythonの内包表記はなぜ速い?この記事によれば、リスト内包表記が、for文でappendするより速い理由はlistオブジェクトからappend属性を取り出さなくて良い関数呼び出しがないの2点らしい。後者はappendを呼びださなくて良いということ。前者の、属性の取り出しが不要であることが、大きく貢献しているらしい。そこで、理解のためにまず上の記事を再現してみる。内部でどんな処理になっているのか確かめるために、Pythonのバイドコートを逆アセンブルするdisというモジュールを使う。結果を見るとリスト内包表記も、forでゴリゴリ回すというのは同じらしい。注目すべきは、上の記事で言われている、append属性の取得と関数呼び出しの部分がどうなっているかだ。普通のループの方を見ると、loop関数の4行目を実行する段階でLOAD_ATTRを実行している。その後に、CALL_FUNCで関数を呼び出している。一方で、リスト内包表記の方を見ると、LIST_APPENDを実行しているだけだ。ここで、属性の取得がどれだけ重いのか見るために、以下の関数を使う。l.appendappend属性をひたすら取得している。これを先ほどのloop関数と実行時間を比較する。#append属性の取得だけを行うループ9.775523900985718実行時間の大体6割くらいが、append属性の取得によるものだった。確かにこの部分がなければ速くなりそう。属性の取得が重い処理だということは分かったけど、どうして重いのか。その点に少し触れている論文があった。inefficient.http://www.cs.ucsb.edu/research/tech_reports/reports/2010-14.pdfそれから稲田さんの記事によれば、この辺の実装はceval.cになるので、少し覗いてみた。Pythonでは、属性の取得をするとき、オブジェクトと属性名が渡される。そこでまず、オブジェクトの型を解決する必要がある。オブジェクトの型が分かると、今度は属性を探す。クラスの属性を見て、見つかればスタックに追加する。そのあと、インスタンスの属性を見て、見つかればスタックに追加する。これをループの中でぐるぐる回して、見つからなくなればbreakする。最後に、スタックからpopして属性を取得する、という流れになる。ここで取得した属性を、キャッシュせずにループの中で毎回行なっているそうな。これは、ループの中で属性が変更されたり、参照先のオブジェクトが変わったりするから。いうわけで、普通にループで回すときは、LOAD_ATTRで属性を取得して、取得した属性をCALL_FUNCで呼び出すという処理を毎回行なっている。一方で、リスト内包表記はリストにappendすることが分かっているので、直にLIST_APPENDを実行できるということ。まとめリスト内包表記は、append属性の取得がない分、forループでappendするより速い。稲田さんの記事にもある通り、属性の取得がなければforループもそこそこ速い。range(i))のような処理は、リスト内包表記より速い場合が多い。それにリスト内包表記より読みやすい。そこで、なんとなく以下のような基準で書けば良い気がする。mapやfilterが使える場合は使うそれ以外で、リスト内包表記で簡単に書ける場合はリスト内包表記を使うもし複雑になるときは可読性のためにforループを使うforループを使うとき、入力サイズが大きければ属性の取得をループから掃き出すつまり実行速度も大切だけど、Pythonなんだから可読性も大切にしたいよね。いうわけで、disモジュールを使ってリスト内包表記をdisってみたのだった。2012の動画を見てたら面白いもの見つけたのでメモ。(1)はてなブログをはじめよう!shkhさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。