Pythonで独自比較関数を用いてソートに適用する
Pythonでリストをソートするとき、独自の比較関数を使いたかった。
そのことについて調べたことをまとめます。
TL;DR
- Python2.xまではsort関数のcmp キーワード引数に独自compare関数を指定すればよかった
- Python3.xでは、sort関数からcmp キーワード引数が完全に削除された
- Python3.xで独自compare関数
custom_compare
を使う場合はsorted(your_list, key=functools.cmp_to_key(custom_comare))
とする - 単純な二重配列の場合
sorted(your_list, key=operator.itemgetter(0,1,2))
のような書き方もできる
事の発端
ソースコードを解析するスクリプトを書いているときに、以下のような二重配列のデータ構造を作った。
list_buff = [ ['fuga', 69, 82], ['fuga', 85, 92], ['hoge', 93, 99], ['fuga', 115, 137], ['hoge', 140, 152] ]
これを、
list_buff[n][0]
は辞書順list_buff[n][1]
は昇順
といった具合にソートしたかった。
具体的には、以下のように直したい。
list_buff = [ # 辞書順, 昇順, スコープ外 ['fuga', 69, 82], ['fuga', 85, 92], ['fuga', 115, 137], ['hoge', 93, 99], ['hoge', 140, 152] ]
C#とかだとcompareクラスを作れば、わりかしsortの自由が利く。
以下はC#のイメージ。
using System; using System.Collections; using System.Collections.Generic; namespace CustomSort { public class RowData { public string Label { get; set; } public int Start { get; set; } public int End { get; set; } public static RowData Create(string label, int start, int end) { return new RowData() { Label = label, Start = start, End = end }; } } public class CustomCompare : System.Collections.Generic.IComparer<RowData> { public int Compare(RowData elm1, RowData elm2) { int diff = elm1.Label.CompareTo(elm2.Label); if (diff != 0) { return diff; } return elm1.Start.CompareTo(elm2.Start); } } class EntryPoint { static void Main(string[] args) { // メイン処理イメージ List<RowData> listBuff = new List<RowData>() { RowData.Create("fuga", 69, 82), RowData.Create("fuga", 85, 92), RowData.Create("hoge", 93, 99), RowData.Create("fuga", 115, 137), RowData.Create("hoge", 140, 152) }; CustomCompare comp = new CustomCompare(); listBuff.Sort(comp); foreach(RowData rd in listBuff) { System.Console.WriteLine(@"{0}: {1}: {2}", rd.Label, rd.Start, rd.End); } } } }
Pythonでも、以下のようなことをやりたい。
if __name__ == '__main__': list_buff = get_datas() def custom_compare(elm1, elm2): if elm1[0] > elm2[0]: return 1 elif elm1[0] < elm2[0]: return -1 else: return elm1[1] - elm2[1] # # こういうイメージ # list_buff.sort(compare=custom_compare)
Pythonにもないかしらと調べた。
結果
公式ドキュメントを調べて sort(cmp=custom_function)
で実現できそうだとわかった。
しかし、上記の文法は2.xまでしかサポートされておらず、3.xではcmp キーワード引数は完全に削除されたとのこと。
3.xで、独自に拡張したcompare関数を使いたい場合、keyキーワード引数にcompare関数をラップしたクラスを渡せばいい。
if __name__ == '__main__': list_buff = [ ['fuga', 69, 82], ['fuga', 85, 92], ['hoge', 93, 99], ['fuga', 115, 137], ['hoge', 140, 152] ] def custom_compare(elm1, elm2): if elm1[0] > elm2[0]: return 1 elif elm1[0] < elm2[0]: return -1 else: return elm1[1] - elm2[1] def cmp2key(comp_func): class CustomCompare: def __init__(self, obj, *arg): self.obj = obj # 以下の__eq__のように、eq, ne, lt, gt, le, ge を定義する def __eq__(self, other): return comp_func(self.obj, other.obj) == 0 ... # cmp2keyのreturn return CustomCompare # 独自のソートを実施 list_buff.sort(key=cmp2key(custom_compare))
ミソは、C#の時と似たような感じ。compare関数を持つクラスを作成すること。
ただ、C#と違うのは、拡張比較演算子をオーバーライドすること。
正直、めんどくさい。
代わりに, functoolsモジュールにあるcmp_to_key関数を使えば、実現したいことを実現できることがわかった。1
from functools import cmp_to_key if __name__ == '__main__': list_buff = [ ['fuga', 69, 82], ['fuga', 85, 92], ['hoge', 93, 99], ['fuga', 115, 137], ['hoge', 140, 152] ] def custom_compare(elm1, elm2): if elm1[0] > elm2[0]: return 1 elif elm1[0] < elm2[0]: return -1 else: return elm1[1] - elm2[1] # 独自のソートを実施 list_buff.sort(key=cmp_to_key(custom_compare))
簡単。
ただ、cmp_to_keyが書かれた箇所の少し上に、もっと単純な書き方が載っていた2。
特に今回のような二重配列の場合、以下の使い方が最も簡単。
from operator import itemgetter if __init__ == '__main__': list_buff = [ ['fuga', 69, 82], ['fuga', 85, 92], ['hoge', 93, 99], ['fuga', 115, 137], ['hoge', 140, 152] ] list_buff.sort(key=itemgetter(0,1))