集めたデータを点数の高い順に並べたい。そう思ってネットを調べたら、sortとsortedの2つが出てきて手が止まった経験はありませんか。
さらにkeyという引数が出てきて、そこにlambdaという見慣れないものが入っている。ここで一度あきらめてしまう方は、とても多い印象です。
でも安心してください。並べ替えの仕組みは、覚えることがとても少ないのです。
この記事では、Pythonの並べ替えを手元で動かせるコードと一緒に、順番に整理していきます。
sortとsortedは、結果の返し方が違うだけ¶
まず押さえたいのは、この2つが別々の機能ではないという点です。やっている並べ替えは、まったく同じものです。
違うのは結果の受け取り方だけ。list.sortは元のリストそのものを書き換え、sortedは並べ替えた新しいリストを返します。
言葉より、動かしたほうが早いです。次のコードを写してみてください。
scores = [72, 95, 58, 88]
# sorted は新しいリストを返す。元のリストはそのまま
new_list = sorted(scores)
print(new_list) # [58, 72, 88, 95]
print(scores) # [72, 95, 58, 88]
# sort は元のリストを書き換える。戻り値は None
result = scores.sort()
print(scores) # [58, 72, 88, 95]
print(result) # None
注目してほしいのは、最後のNoneです。sortが並べ替えた結果を返さないのは、元のリストを書き換えたことをはっきりさせるためだと公式ドキュメントは説明しています。
つまりsorted_list = my_list.sort()と書くと、中身はNoneになります。これは初心者がほぼ全員通る道です。
2つの違いを表にまとめておきます。迷ったときはここに戻ってきてください。
| list.sort() | sorted() | |
|---|---|---|
| 使える対象 | リストだけ | あらゆるイテラブル(タプル、集合、辞書など) |
| 元のデータ | 書き換わる | そのまま残る |
| 戻り値 | None | 並べ替えた新しいリスト |
| メモリ | 追加のリストを作らない | 新しいリストの分だけ増える |
リストとタプルの性質そのものが曖昧な方は、先にこちらを読んでおくと違いがすっと入ってきます。【関連記事】Pythonのリスト(list)とタプル(tuple)、どっちを使う? それぞれの違いを徹底解説
迷ったらsortedを選んでおけば失敗しにくい¶
では実際にはどちらを使うべきか。私は基本的にsortedをすすめています。
理由は単純で、元のデータが残るからです。並べ替えたあとに元の順番が必要になっても、慌てずに済みます。
sortedのもうひとつの強みは、リスト以外も受け取れることです。タプルでも集合でも、辞書でも通ります。
# タプルを渡してもリストが返ってくる
print(sorted(("banana", "apple", "cherry")))
# ['apple', 'banana', 'cherry']
# 辞書を渡すとキーが並ぶ
stock = {"pen": 3, "book": 12, "eraser": 7}
print(sorted(stock))
# ['book', 'eraser', 'pen']
# 値で並べたいときは items() を渡す
print(sorted(stock.items(), key=lambda item: item[1]))
# [('pen', 3), ('eraser', 7), ('book', 12)]
sortを使う価値があるのは、扱うリストが非常に大きく、新しいリストを作るメモリが惜しい場面くらいです。数百件や数千件なら、体感できる差はまず出ません。
key引数は、何を基準に比べるかの指定¶
ここからが本題です。key引数は、並べ替えの主役といっていい機能です。
やっていることは一言で説明できます。要素そのものではなく、要素をkeyに通した結果どうしで比べる、ただそれだけです。
たとえば文字列を長さ順に並べたいとき。文字列同士をそのまま比べるとアルファベット順になってしまいます。
words = ["banana", "fig", "cherry", "kiwi"]
print(sorted(words))
# ['banana', 'cherry', 'fig', 'kiwi'] ← 辞書順
print(sorted(words, key=len))
# ['fig', 'kiwi', 'banana', 'cherry'] ← 長さ順
key=lenと書くだけで、比べる基準が文字数に変わりました。ここで渡しているlenは、呼び出す前の関数そのものです。
len()のようにカッコを付けてしまうと、その場で実行された結果が渡ってしまいます。カッコを付けないのがポイントです。
lambdaは、その場かぎりの小さな関数¶
辞書のリストのように、要素が複雑なときはlambdaの出番です。lambdaは名前を付けずに使う小さな関数だと思ってください。
students = [
{"name": "sato", "score": 72, "grade": 2},
{"name": "endo", "score": 95, "grade": 1},
{"name": "kato", "score": 72, "grade": 1},
]
# 点数の高い順に並べる
for s in sorted(students, key=lambda s: s["score"], reverse=True):
print(s["name"], s["score"])
lambda s: s["score"]は、要素sを受け取ってs["score"]を返すだけの関数です。これをdefで書くと3行かかるところが、1行に収まります。
lambdaの書き方そのものに自信がない方は、こちらで基礎から確認できます。【関連記事】ラムダ式(無名関数)を使いこなす。コードを極限までシンプルにする方法を解説
reverseは逆順にするためのスイッチ¶
上のコードにさりげなく登場したreverseにも触れておきます。TrueにすればsortもsortedもDESC、つまり大きい順になります。
reversedという別の関数と混同しやすいので気をつけてください。reversedは並べ替えをせず、今の並びをただひっくり返すだけです。
安定ソートを知ると、複数条件の並べ替えが楽になる¶
実務でよくあるのが、学年順に並べたうえで、同じ学年の中では点数順にしたいという要求です。ここで効いてくるのが安定ソートという性質になります。
公式ドキュメントには、Pythonのソートは安定であることが保証されると明記されています。同じ値だった要素同士は、元の順番のまま保たれるという意味です。
この性質があるので、優先度の低い条件から順に、2回に分けて並べ替えれば目的が達成できます。
# 1回目: 弱い条件(点数の高い順)
students.sort(key=lambda s: s["score"], reverse=True)
# 2回目: 強い条件(学年の昇順)
students.sort(key=lambda s: s["grade"])
for s in students:
print(s["grade"], s["score"], s["name"])
# 1 95 endo
# 1 72 kato
# 2 72 sato
学年で並べ直しても、点数の並びが崩れていません。これが安定ソートの恩恵です。
もちろん、タプルを返すkeyで一度に書く方法もあります。key=lambda s: (s["grade"], -s["score"])のように書けば1行で済みます。
ただし数値以外はマイナスを付けられないため、降順が混ざるときは2回に分ける書き方のほうが素直です。私は読みやすさを優先して、後者を選ぶことが多いです。
itemgetterを使うと、書き方がもう一段短くなる¶
lambdaに慣れてきたら、標準ライブラリのoperatorも覚えておくと便利です。itemgetterとattrgetterという2つを押さえれば十分でしょう。
用途ごとの書き分けを表にしておきます。
| やりたいこと | lambdaで書く場合 | operatorで書く場合 |
|---|---|---|
| 辞書のキーで並べる | lambda s: s["score"] |
itemgetter("score") |
| タプルのn番目で並べる | lambda t: t[1] |
itemgetter(1) |
| オブジェクトの属性で並べる | lambda p: p.age |
attrgetter("age") |
| 複数条件をまとめる | lambda s: (s["grade"], s["name"]) |
itemgetter("grade", "name") |
itemgetterはC言語で実装されているぶん、lambdaより速いとされています。とはいえ普段の開発で差を体感する場面はほとんどありません。
読みやすいと感じるほうを選んでください。データ構造をdataclassで定義しているなら、attrgetterとの相性がとても良いです。【関連記事】Pythonのdataclassとは?クラスの定義がぐっと短くなる書き方を初心者向けに解説
並べ替えの裏側では何が起きているのか¶
少しだけ内部の話もしておきます。知らなくても書けますが、知っていると速度の見積もりができるようになります。
Pythonの並べ替えは、すでに順番が揃っている部分をうまく利用する賢いアルゴリズムで動いています。長らくTimsortという名前で知られてきた方式です。
Python 3.11では、その中の統合ルールがPowersortという方式に置き換わりました。多くのコードでは体感差は出ませんが、極端に不利だったデータの並びでは改善が見込めます。
計算量の目安はO(n log n)です。この記号の読み方があやしい方は、こちらで整理しておくと処理時間の予測が立てられるようになります。【関連記事】「実行時間が終わらない…」を卒業する!あなたのコードを100倍速くする計算量の考え方
ひとつ覚えておきたいのは、key関数は各要素につき1回しか呼ばれないということです。だからkeyの中に重い処理を書いても、比較のたびに実行されるわけではありません。
実務でつまずきやすいポイント¶
最後に、現場で見かける失敗をいくつか共有します。どれも一度知っておけば避けられるものばかりです。
まず、型が混ざったリストは並べ替えられません。数値と文字列が同居しているとTypeErrorになります。
私は10年ほどエンジニアとして開発に関わってきましたが、この手のエラーで一番手こずったのは、CSVから読んだ数字が文字列のまま混ざっていたときでした。見た目は数字なのに文字列として並ぶので、10が9より前に来る不思議な結果になり、原因にたどり着くまで時間を溶かしました。
次に、日本語の並べ替えです。ひらがなや漢字はUnicodeの番号順に並ぶだけで、五十音順にはなりません。
読み仮名の列を別に持ち、それをkeyに指定するのが現実的な解決策です。地味ですが、これが一番確実でした。
そして大文字と小文字の扱いにも注意が必要です。英数字はASCIIの順に並ぶため、大文字がすべて小文字より前に来てしまいます。
区別せずに並べたいならkey=str.lowerを指定してください。1行足すだけで、人間の感覚に近い並びになります。
よくある失敗と対処法を表にまとめておきます。
| つまずきポイント | 起きること | 対処 |
|---|---|---|
x = my_list.sort() |
xがNoneになる | sortedを使うか、sort後に元の変数を使う |
| 数値と文字列が混在 | TypeErrorで止まる | int()などで型を揃えてから並べる |
| 日本語が五十音順にならない | Unicode順で並ぶ | 読み仮名の列を用意してkeyに指定する |
| 大文字が先に固まる | ASCII順で並ぶ | key=str.lowerを指定する |
key=len()と書く |
すぐにエラーになる | カッコを外してkey=lenと書く |
今日から使える形にまとめておく¶
ここまでの内容を、実際に書くときの判断だけに絞って整理します。
元のデータを残したいならsorted、その場で書き換えてよいならsort。迷ったらsortedで問題ありません。
並べる基準を変えたいならkeyを渡す。keyには関数を渡すのであって、呼び出した結果を渡すのではありません。
複数の条件で並べたいなら、弱い条件から順に2回並べ替える。安定ソートが保証されているからこそ使える手です。
そして、逆順はreverse=Trueで指定する。reversedとは別物だと覚えておいてください。
まずは手元のリストにkey=lenを付けるところから試してみてください。基準を差し替えるだけで結果が変わる感覚がつかめれば、あとは応用するだけです。
同じ標準ライブラリの中では、集計と並べ替えを組み合わせる場面でcollectionsもよく登場します。【関連記事】Pythonのcollectionsとは?Counterやdefaultdictで毎日のコードが短くなる使い方を解説
ここまでお読みいただきありがとうございました。