[Pythonクイズ]リストの要素を逆順に並べ替える方法、何個思い付きますか?:Pythonステップアップクイズ
「リストの要素を逆順に」と聞かれたら、どんな方法をどれくらい思い付きますか? あの方法やその方法は誰でも分かるでしょうけど、こんなやり方もあるかもしれません。というのを考えてみましょう。
【問題】
リストの要素を逆順に並べ替える方法は何個あるか、考えてみてほしい。例えば、「mylist = [2, 8, 3, 7, 1, 2, 1]」として作成したリストの要素を逆順に並べ替えると「[1, 2, 1, 7, 3, 8, 2]」となる。以下はすぐに思い付くであろうリストのreverseメソッドを使ったコードの例だ。なお、新規にリストを生成してもよいし、元のリストをインプレースで変更してもよい。
mylist = [2, 8, 3, 7, 1, 2, 1]
mylist.reverse()
print(mylist) # [1, 2, 1, 7, 3, 8, 2]
どうもHPかわさきです。今回の問題はクイズとしてはどうなのよ? というモノになってしまいました(またですか)。リストの要素を逆順に並べ替える方法にはバリエーションがたくさんあり過ぎて「それはX個ですね」なーんてことは簡単にはいえないんですよね。
最初は「2個」「4個」みたいな選択肢を用意しようと思いましたがあきらめました。なので、読者の皆さんも「こんな方法があるよね」「これはイケるかな?」みたいな感じでリストの要素を逆順に並べ替える方法を試していただければと思います。
筆者は以下の答えを思い付きましたが、「他にもこんなのがあるよ!」「なんでコレがないの?」なんて答えを思い付く人もいるはずです。そんなものがあれば、HPかわさきまで教えてくださいね。
では、HPかわさきが思い付いたやり方を発表します。デロデロデロデロー(ドラムロール)。
【答え】
以下は筆者(HPかわさき)が思い付いたやり方です。
- reverseメソッドを使う
- reversed関数を使う
- スライスで「[::-1]」を指定する
- 内包表記を使う
- for文とリストのメソッドを組み合わせる
- while文とリストのメソッドを組み合わせる
- sorted関数とenumerate関数を組み合わせる
5と6を見て「フワフワした表現だなぁ」と思いましたか? でも、リストの要素をインデックスアクセスで取り出して結果のリストにinsertメソッドで挿入するやり方もあれば、popメソッドで破壊的にリストから要素を取り出して結果のリストにappendメソッドで追加するやり方もあるわけで、ここではそれらをひとまとめにしています。「いやいや、それらは明確に違うやり方でしょっ!」という人もいらっしゃるかもしれませんが、ここではそういうことで一つお願いします。
以下の解説の後半で、これらのやり方の速度を比較してみます。どれを使うかを検討する参考にしてみてください。
【解説】
ここからは解説というか、それぞれのやり方をPythonのコードとして記述してみましょう。
最初は簡単なものとして、次の3つのコードを紹介します。
- reverseメソッドを使う
- reversed関数を使う
- スライスで「[::-1]」を指定する
以下に3つのやり方のコードをまとめて示します。
# 1. reverseメソッド
mylist = [2, 8, 3, 7, 1, 2, 1]
mylist.reverse()
print(mylist) # [1, 2, 1, 7, 3, 8, 2]
# 2. reversed関数
mylist = [2, 8, 3, 7, 1, 2, 1]
result = list(reversed(mylist))
print(result) # [1, 2, 1, 7, 3, 8, 2]
# 3. スライス
mylist = [2, 8, 3, 7, 1, 2, 1]
result = mylist[::-1]
print(result) # [1, 2, 1, 7, 3, 8, 2]
覚えておくべきはreverseメソッドはインプレースでリストの要素を並べ替えることと、reversed関数の戻り値がリストではなくイテレーターであることと(リストにしたければ戻り値をlist関数に渡す)でしょうか。「[::-1]」というスライスを使う方法は、開始位置と終了位置を省略して、差分を-1にすることで逆順に並んだ要素を取得することを意味しています。
次に4の内包表記を使う例です。
# 4. 内包表記
mylist = [2, 8, 3, 7, 1, 2, 1]
result = [mylist[-idx] for idx in range(1, len(mylist) + 1)]
print(result) # [1, 2, 1, 7, 3, 8, 2]
mylist = [2, 8, 3, 7, 1, 2, 1]
result = [mylist.pop() for _ in range(len(mylist))]
print(result) # [1, 2, 1, 7, 3, 8, 2]
あ、ここでもう2つのバリエーションが出てきましたね。1つ目のコードは「len関数でリストの要素数を調べて、range関数で1始まりのインデックスを反復するようにし、それをマイナスのインデックスとして指定することで末尾から要素を取り出していく」処理を内包表記として表現しています。
2つ目のコードは「リストの要素数だけ、popメソッドで末尾から要素を取り出していく」処理を内包表記として表現しています。こちらは元のリスト(mylist)を破壊する点に注意が必要です。
筆者はこの2つの方法を思い付いたところで満足してしまいましたが、他の方法もあるかもしれません。
5のfor文とリストのメソッドを組み合わせる方法もバリエーション含めて見てみましょう。
# 5. forループを使うもの
mylist = [2, 8, 3, 7, 1, 2, 1]
result = []
for item in mylist:
result.insert(0, item)
print(result) # [1, 2, 1, 7, 3, 8, 2]
result = []
for _ in range(len(mylist)):
result.append(mylist.pop())
print(result) # [1, 2, 1, 7, 3, 8, 2]
最初の例はリストの要素を反復して、それらを結果のリストの先頭に挿入するやり方です。これは元のリストを破壊しません。次の例は内包表記の2つ目のコードをfor文に書き下したと考えられますね。popメソッドを使っているので、元のリストは破壊されます。
6の「while文とリストのメソッドを組み合わせる」やり方の例を以下に示します。
# 6. while文とpopメソッド
mylist = [2, 8, 3, 7, 1, 2, 1]
result = []
while mylist:
result.append(mylist.pop())
print(result) # [1, 2, 1, 7, 3, 8, 2]
バリエーションも考えましたが、これが一番簡潔かなぁと思います。基本的な考え方はfor文の2つ目の例と同じです。ポイントはwhile文の終了条件がリストが空かどうかになっていることくらいでしょうか。
sorted関数に並べ替えのキーとしてインデックスを指定すれば、リストの要素を逆順に並べ替えられるんじゃない? という考えを反映したのが、7の「sorted関数とenumerate関数を組み合わせる」やり方です。実際のコードは次のようになります。
# 7. sorted関数とenumerate関数を組み合わせる
mylist = [2, 8, 3, 7, 1, 2, 1] # リストに同じ値が複数個ある
result = [x for _, x in sorted(enumerate(mylist), reverse=True)]
print(result) # [1, 2, 1, 7, 3, 8, 2]
sorted関数のkeyパラメーターには並べ替えのキーを指定できますが、このキーは引数を1個受け取る関数(あるいは式)でなければなりません。そして、並べ替えの際にはリストの要素が順次渡されるような設計になっています。そのため、リストをそのまま渡しても、個々の要素からそのインデックスを得ることができないのが、この方法の難点です。
そこで上のコードでは、リストをenumerate関数に渡して、そのインデックスと値から成るタプル(のシーケンス)をsorted関数に渡して、逆順に並べ替え、今度はその値だけを取り出す内包表記に含めるようにしています。長ったらしい処理ですが、確かにこれでも逆順に並べ替えられます。
ここではkeyパラメーターの指定をしていませんが、要素として「(インデックス, 値)」というタプルが渡されるので、「key=lambda x: x[0]」のようにタプルの第0要素を並べ替えのキーにするという指定をしても同じです。
個々の処理に分けて、上でやっていることを記述したのが以下です。
e = enumerate(mylist)
tmp = list(e)
r0 = sorted(tmp)
print(r0) # [(0, 2), (1, 8), (2, 3), (3, 7), (4, 1), (5, 2), (6, 1)]
r1 = sorted(tmp, reverse=True)
print(r1) # [(6, 1), (5, 2), (4, 1), (3, 7), (2, 3), (1, 8), (0, 2)]
r2 = [x for _, x in r1]
print(r2) # [1, 2, 1, 7, 3, 8, 2]
enumerate関数にリストを渡すと、そのインデックスと値を要素とするタプルを反復するイテレーターが生成されます。それをここではlist関数に渡してリストにしています。sorted関数にそのリストをそのまま渡すと、タプルの第0要素であるインデックスをキーとして並べ替えが行われます。「print(r0)」の結果がそうです。インデックス順に要素が並べ替えられている(結果としては並べ替えられていない)のが分かります。
そこでsorted関数でreverse=Trueを指定することで、これを逆順に並べ替えられます。その結果が「print(r1)」で分かります。
インデックスは不要なので、最後にタプルを要素とするリストから、値だけを内包表記と取り出せば逆順に並べ替えられたリストのできあがり! というわけですね。
実行時間を計測してみよう
とここまでは、何種類かのやり方を紹介してきました。ここで終わってもよいのですが、実行時間を計測して、どれが一番良さそうかについて少しだけ見てみましょう。ざっくりとコードを示します。
from time import time
from random import randint
def time_reverse_list(func, data_count=10000, exec_count=10000):
mylist = [randint(0, 100) for _ in range(data_count)]
start = time()
for _ in range(exec_count):
func(mylist)
end = time()
return func.__name__, end - start
def use_reverse(mylist):
mylist.reverse()
def use_reversed_func_0(mylist):
result = reversed(mylist)
def use_reversed_func_1(mylist):
result = list(reversed(mylist))
def use_slice(mylist):
result = mylist[::-1]
def use_list_comprehension_0(mylist):
result = [mylist[-idx] for idx in range(1, len(mylist) + 1)]
def use_list_comprehension_1(mylist):
tmp = mylist.copy()
result = [tmp.pop() for idx in range(len(tmp))]
def use_for_loop_0(mylist):
result = []
for item in mylist:
result.insert(0, item)
def use_for_loop_1(mylist):
tmp = mylist.copy()
result = []
for _ in range(len(tmp)):
result.append(tmp.pop())
def use_while_loop(mylist):
tmp = mylist.copy()
result = []
while tmp:
result.append(tmp.pop())
def use_sorted_func(mylist):
result = [x for _, x in sorted(enumerate(mylist), reverse=True)]
funcs = [use_reverse, use_reversed_func_0, use_reversed_func_1, use_slice,
use_list_comprehension_0, use_list_comprehension_1,
use_for_loop_0, use_for_loop_1, use_while_loop, use_sorted_func]
for func in funcs:
name, t = time_reverse_list(func)
print(f'{name[4:]}: {t:.6f}')
time_reverse_list関数は今までに見てきた処理を関数化したものを第0引数に受け取ります。リストの要素数(data_count)と関数の呼び出し回数(exec_count)も指定できますが、デフォルト引数値は10000にしています。ここではデフォルト引数値をそのまま使うことにします。
time_reverse_list関数の後には今までに見てきた処理を関数にしたものがズラリと並んでいます。ただし、元のリストを破壊する処理については最初にリストをコピーするようにしています(コピーにかかる時間も実行時間として計測されている点には注意してください)。
後は各関数を要素とするリストを作成して、forループでtime_reverse_list関数にそれらを渡して、実行時間を表示するだけです。
実行結果を以下に示します。
やり方 | 時間(合計) |
---|---|
reverseメソッド | 0.044444 |
reversed関数(リスト化しない) | 0.001409 |
reversed関数(リスト化する) | 0.406508 |
スライス | 0.132200 |
内包表記(insertメソッド) | 4.659034 |
内包表記(popメソッド) | 3.333162 |
forループ(insertメソッド) | 135.674039 |
forループ(popメソッド) | 4.182015 |
whileループ | 3.755522 |
sorted関数 | 15.654806 |
実行時間の計測結果 |
パッと見たところ、forループでインデックスアクセス+insertメソッドでリストの先頭に要素を挿入していくことで逆順に並べ替えるやり方が圧倒的な遅さを示しました。リストの先頭への挿入がコストのかかる処理なのでしょうか。
それを確かめるために、次のコードを実行してみました。
def use_for_loop_2(mylist):
result = []
for idx in range(1, len(mylist)+1):
result.append(mylist[-idx])
time_reverse_list(use_for_loop_2)
こちらはrange関数とfor文を使って、リストの末尾から順に要素を取り出し、それをリストの末尾にappendメソッドで追加していくコードです。要素の反復をやめてインデックスで末尾から要素にアクセスしている点と、リストの先頭にinsertメソッドで挿入するのをやめてリストの末尾にappendメソッドで追加している点が異なっているところです。
この実行結果は内包表記やもう1つのforループ(popメソッドとappendメソッドを使用)などと大体同じ時間で終わりました。というわけで、リストの先頭への挿入は時間コストが高い処理なのかもしれませんね。
となると、dequeオブジェクトが気になります。dequeオブジェクトは先頭と末尾への挿入が高速なリストライクなオブジェクトです。
from collections import deque
def use_deque(mylist):
result = deque()
for item in mylist:
result.appendleft(item)
time_reverse_list(use_deque)
このやり方だと、すごーく高速化されるのかと思ったのですが、内包表記やappendメソッドを使ったforループとおおよそ似たような時間(ただし、若干高速)という結果になりました。
これら2つのやり方を含めて実行時間の計測結果を以下に示します。
やり方 | 時間(合計) |
---|---|
reverseメソッド | 0.044444 |
reversed関数(リスト化しない) | 0.001409 |
reversed関数(リスト化する) | 0.406508 |
スライス | 0.132200 |
内包表記(insertメソッド) | 4.659034 |
内包表記(popメソッド) | 3.333162 |
forループ(insertメソッド) | 135.674039 |
forループ(popメソッド) | 4.182015 |
whileループ | 3.755522 |
sorted関数 | 15.654806 |
forループ(その2) | 4.999490 |
dequeオブジェクト | 3.054137 |
実行時間の計測結果 |
速いのはreverseメソッドとreversed関数(リスト化してもしなくても)、スライスとして「[::-1]」を指定という4つのやり方でした。リストの先頭にinsertするfor文がとりわけ遅かったのは上で見た通りです。他は似たり寄ったりの結果ですが、sorted関数を使った方法はまあまあ遅いですね。コードを書きながら遅そうと思っただけのことはあります(笑)。
というわけで、結論はリストの要素を逆順に並べ替える方法はいろいろあるけれど、速度を求めるならPythonのエライ人たちが用意してくれたreverseメソッドやreversed関数、スライス指定を使えってこったということですね。
リストの要素の操作については「Python入門」の第17回「リストの操作」でも取り扱っています(sorted関数についても触れています)。また、dequeオブジェクトについては「解決!Python」の「dequeオブジェクトを使うには」で紹介しています。いつものことながら、興味のある方はそちらもぜひご覧ください。
逆順に並べ替えるもっと良い方法があるよ、という方はHPかわさきまでメンションなりなんなりいただけるとうれしいです。
Copyright© Digital Advantage Corp. All Rights Reserved.