静かなる名辞

pythonとプログラミングのこと


【python】反転させて先頭n個取るスライス

 タイトルの通りのものが必要になりました。一体どう書くのでしょう?

とりあえず反転させる

>>> lst = list(range(20))
>>> lst[::-1]
[19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

 ま、これは常識(python廃人の皆さんには)。

n個取ってみる

>>> lst[::-1][:10]
[19, 18, 17, 16, 15, 14, 13, 12, 11, 10]

 こうするとリストオブジェクトの生成が二回繰り返されるので遅いはずです。

 できればスライス一発で済ませたい。

やってみる

>>> lst[:9:-1]
[19, 18, 17, 16, 15, 14, 13, 12, 11, 10]

 なんとなくできたような気になりますが、

>>> lst[:3:-1]
[19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4]

 逆向きなのでした。スライスも左から順に評価される(切り出してから反転)のでしょうか。

正しい(?)やりかた

>>> lst[:-4:-1]
[19, 18, 17]

 微妙な違和感が・・・

先頭n個取って反転

 これもやってみよう。考え方は同じです。

>>> lst[2::-1]
[2, 1, 0]

測ってみた

>>> import timeit
>>> timeit.timeit(lambda : lst[::-1][:3])
0.35431641300010597
>>> timeit.timeit(lambda : lst[:-4:-1])
0.20682314900022902

 計測誤差ではないようです。かくして約43%の高速化が実現されました。

追記:書き上げて投稿してから思いついた別解

>>> lst[:-4:-1]
[19, 18, 17]
>>> lst[-3:][::-1]
[19, 18, 17]

 結果は同じ。これだと反転するリストが小さいので速いのでは? と思いました。

 効果を見やすくするために、長さ1000のリストで試してみます。

>>> timeit.timeit(lambda : lst[:-4:-1])
0.21827364299997498
>>> timeit.timeit(lambda : lst[::-1][:3])
2.8037329549997594
>>> timeit.timeit(lambda : lst[-3:][::-1])
0.33725221500026237

 早い順に、

  1. 1つのスライスで反転と切り出しをやる
  2. 後ろ3つを取ってから反転
  3. ぜんぶ反転させてから先頭3つを取る

 となりました。

 「ぜんぶ反転させてから~」が遅いのはまあ、予想通りですが、「後ろ3つを取ってから~」は1つのスライスでやる方法に勝てておらず、リストが小さいときの「ぜんぶ反転させてから~」と同程度。つまりオーバーヘッド分が同程度あるということです。また、built-inのスライスはそうとう気が効いてるみたいです(恐らく、lenを見てどの順番で切っていくか決めているのでは? 確認していませんが・・・)。

 結論としては、とにかくできるだけ1つのスライスで書いちゃおう、ということになると思います。ただし、実際には大した時間じゃないので、可読性重視で分けるのも不可ではない、という程度です。