多重キーでのソート

多次元リストのソートってよく使う機能だと思うんですが、pythonのソートってよく考えて作ってあるなぁと関心しました。

例えば、次のような「生徒の成績リスト」があったとします。

# 配列の並び順 [名前],[国語],[算数]
a = [
   ['Tim',     55, 46 ],
   ['Jack',    55, 70 ],
   ['Mathhew', 23, 80 ],
]

そこで、リスト2番目の値である「国語」を、点数が高い順にソートするしたい場合は、以下の用になります。

>>> sorted(a, key=lambda x:x[1], reverse=True)
[['Tim', 55, 46], ['Jack', 55, 70], ['Mathhew', 23, 80]]

ちなみにcmpパラメータではなくkeyパラメータを上書きしたのは高速化のためです、念のため・・・

結果を見ると「Tim」と「Jack」は、国語は55点で同点ですが、算数は「Jack」の方が上です。でも、国語が同点だった場合は、算数の高い方が上にソートしたい場合がありますよね。
つまり、第1キー(国語)と第2キー(算数)による多重キーのソートです。

結論から言いますと、多重キーのソートは以下のとおりです。

>>> sorted(a, key=lambda x:(x[1],x[2]), reverse=True)
[['Jack', 55, 70], ['Tim', 55, 46], ['Mathhew', 23, 80]]

変わった箇所はkeyパラメータに、キーの優先順位順に要素を並べただけです、もの凄い簡単です。

なんでこうなるかと言いますと、標準の比較関数であるcmp()に秘密があります。
cmp(x,y)とは、x>yだと正の数、x=yだと0、x<yだと負の数を返すビルトイン関数です。

>>> cmp(0,1)
-1
>>> cmp(1,1)
0
>>> cmp(1,0)
1

このcmp()って、実は引数としてリストも受け付けてくれるんです。
リストの若い要素順に値を比較していき、決着が着くまで比較してくれます。
つまり、こういう事ですね。

cmp([第1キー,第2キー,...],[第1キー,第2キー,...])

ですからkeyパラメータを、キーの優先順位に要素が並んだリスト(タプル)を返す関数を与えれば、多重キーのソートができるのです。

でもデフォルトのkeyパラメータは、多重リストを与えられた場合はリストを返すようになっています。
つまり、キーの優先順位で並べられたリストを与えれば、特別な設定をしなくても、多重キーでのソートになっているのです。

>>> #[第1キー],[第2キー]で並んでいるリスト
... a = [
...     [1,4],
...     [1,3],
...     [1,2],
...     [2,2],
...     [2,1],
... ]
>>> 
>>> #特に設定をせずにソートすれば多重キーのソートとなる
... sorted(a)
[[1, 2], [1, 3], [1, 4], [2, 1], [2, 2]]

ほぉぉ〜、pythonってよくできていますね。
他の言語はどうなっているんでしょうねぇ?
少なくとも、PHPのarray_multisort()は面倒臭いという事だけは知っています(笑)

追記

第1キーは昇順、第2キーは降順にしたい場合があると思います。
そんな時は以下のように、キーに-をつければ降順になります(数値限定です)

>>> #[順位],[点数]
... a = [
...     [1,80],
...     [1,90],
...     [1,20],
...     [2,100],
... ]
>>> 
>>> #第2キーの点数を降順にしたい場合、-をつける
... sorted(a, key=lambda x:(x[0],-x[1]))
[[1, 90], [1, 80], [1, 20], [2, 100]]