Pythonで辞書のリストを特定のキーの値に従ってソート
Pythonで共通のキーを持つ辞書を要素とするリストをsort()メソッドやsorted()関数でソートしようとすると、デフォルトではエラーTypeErrorになってしまう。
sort()メソッドやsorted()関数の引数keyを指定することで辞書のリストを特定のキーの値に従ってソートできる。
共通のキーを持つ辞書のリストはJSONを読み込むと頻繁に遭遇する。PythonでのJSONの読み書きは以下の記事を参照。
また、このような辞書のリストはデータ分析ライブラリpandasのpandas.DataFrameに変換できる。もろもろの処理をするのであればpandas.DataFrameに変換すると便利。
以降のサンプルコードでは、以下のリストを例とする。出力を見やすくするためpprintモジュールを使う。
import pprint
l = [{'Name': 'Alice', 'Age': 40, 'Point': 80},
{'Name': 'Bob', 'Age': 20},
{'Name': 'Charlie', 'Age': 30, 'Point': 70}]
デフォルトでは辞書のリストをソートするとエラー
辞書(dict型オブジェクト)のリストをsort()メソッドやsorted()関数でソートしようとすると、デフォルトではエラーTypeErrorになってしまう。
これは辞書オブジェクトの大小比較(<や>などでの演算)がサポートされていないため。
# sorted(l)
# TypeError: '<' not supported between instances of 'dict' and 'dict'
引数keyに無名関数(ラムダ式)を指定
辞書のリストを特定のキーの値に従ってソートしたい場合は、sort()メソッドやsorted()関数の引数keyを指定する。
keyには、ソートされる(各要素が比較される)前にリストの各要素に適用される関数を指定する。keyに指定した関数の結果に従ってソートされる。詳細は以下の記事を参照。
今回の例ではリストの要素である辞書から特定のキーの値を取得する関数を指定すればよい。
defで関数を定義してもいいが、このような場合は無名関数(ラムダ式)を使うと便利。
pprint.pprint(sorted(l, key=lambda x: x['Age']))
# [{'Age': 20, 'Name': 'Bob'},
# {'Age': 30, 'Name': 'Charlie', 'Point': 70},
# {'Age': 40, 'Name': 'Alice', 'Point': 80}]
pprint.pprint(sorted(l, key=lambda x: x['Name']))
# [{'Age': 40, 'Name': 'Alice', 'Point': 80},
# {'Age': 20, 'Name': 'Bob'},
# {'Age': 30, 'Name': 'Charlie', 'Point': 70}]
降順・昇順は引数reverseで指定する。
pprint.pprint(sorted(l, key=lambda x: x['Age'], reverse=True))
# [{'Age': 40, 'Name': 'Alice', 'Point': 80},
# {'Age': 30, 'Name': 'Charlie', 'Point': 70},
# {'Age': 20, 'Name': 'Bob'}]
ここまでの例はsorted()を使っているが、リストのsort()メソッドでも同様に引数keyや引数reverseを指定できる。
sort()とsorted()の違いについては以下の記事を参照。元のオブジェクト自体を並び替えるのがsort()で、並び替えた新たなオブジェクトを生成するのがsorted()。
共通のキーを持たない要素が存在する場合
上で示した方法だと、共通のキーを持たない要素が存在する場合にエラーとなる。
# sorted(l, key=lambda x: x['Point'])
# KeyError: 'Point'
そのような場合は、存在しないキーに対しても値を取得できる辞書のget()メソッドを使う。
デフォルトでは存在しないキーに対してNoneを返すので、数値や文字列とは比較できずにエラーとなる。
# sorted(l, key=lambda x: x.get('Point'))
# TypeError: '<' not supported between instances of 'int' and 'NoneType'
get()の第二引数に存在しないキーに対して返す値を指定できる。キーが存在しない要素は第二引数に指定した値に置き換えられてソートされる。
pprint.pprint(sorted(l, key=lambda x: x.get('Point', 75)))
# [{'Age': 30, 'Name': 'Charlie', 'Point': 70},
# {'Age': 20, 'Name': 'Bob'},
# {'Age': 40, 'Name': 'Alice', 'Point': 80}]
無限大infは他のどの数値よりも大きいと判定されるので、inf, -infを使うとキーが存在しない要素を常に末尾または先頭に置くことができる。
pprint.pprint(sorted(l, key=lambda x: x.get('Point', float('inf'))))
# [{'Age': 30, 'Name': 'Charlie', 'Point': 70},
# {'Age': 40, 'Name': 'Alice', 'Point': 80},
# {'Age': 20, 'Name': 'Bob'}]
pprint.pprint(sorted(l, key=lambda x: x.get('Point', -float('inf'))))
# [{'Age': 20, 'Name': 'Bob'},
# {'Age': 30, 'Name': 'Charlie', 'Point': 70},
# {'Age': 40, 'Name': 'Alice', 'Point': 80}]
引数keyにoperator.itemgetter()を指定
標準ライブラリのoperatorモジュールのitemgetter()を使う方法もある。ラムダ式よりも高速。
import operator
pprint.pprint(sorted(l, key=operator.itemgetter('Age')))
# [{'Age': 20, 'Name': 'Bob'},
# {'Age': 30, 'Name': 'Charlie', 'Point': 70},
# {'Age': 40, 'Name': 'Alice', 'Point': 80}]
pprint.pprint(sorted(l, key=operator.itemgetter('Name')))
# [{'Age': 40, 'Name': 'Alice', 'Point': 80},
# {'Age': 20, 'Name': 'Bob'},
# {'Age': 30, 'Name': 'Charlie', 'Point': 70}]
共通のキーを持たない要素が存在する場合はエラーとなる。
# sorted(l, key=operator.itemgetter('Point'))
# KeyError: 'Point'
複数のキーを基準にソート
共通のキーに対して同じ値を持つ辞書が存在する場合を例とする。'State'というキーに対して'CA'という値を持つ辞書が2つある。
l_dup = [{'Name': 'Alice', 'Age': 40, 'Point': 80, 'State': 'CA'},
{'Name': 'Bob', 'Age': 20, 'State': 'NY'},
{'Name': 'Charlie', 'Age': 30, 'Point': 70, 'State': 'CA'}]
値が等しいと元の順序が保持される。
pprint.pprint(sorted(l_dup, key=operator.itemgetter('State')))
# [{'Age': 40, 'Name': 'Alice', 'Point': 80, 'State': 'CA'},
# {'Age': 30, 'Name': 'Charlie', 'Point': 70, 'State': 'CA'},
# {'Age': 20, 'Name': 'Bob', 'State': 'NY'}]
operator.itemgetter()には複数の引数を指定でき、最初の値が等しい場合は次の値で比較されソートされる。
pprint.pprint(sorted(l_dup, key=operator.itemgetter('State', 'Age')))
# [{'Age': 30, 'Name': 'Charlie', 'Point': 70, 'State': 'CA'},
# {'Age': 40, 'Name': 'Alice', 'Point': 80, 'State': 'CA'},
# {'Age': 20, 'Name': 'Bob', 'State': 'NY'}]
引数に指定する順番が異なると結果も異なるので注意。
pprint.pprint(sorted(l_dup, key=operator.itemgetter('Age', 'State')))
# [{'Age': 20, 'Name': 'Bob', 'State': 'NY'},
# {'Age': 30, 'Name': 'Charlie', 'Point': 70, 'State': 'CA'},
# {'Age': 40, 'Name': 'Alice', 'Point': 80, 'State': 'CA'}]
ラムダ式で複数の値をタプルやリストで返しても同様の処理が可能。
pprint.pprint(sorted(l_dup, key=lambda x: (x['State'], x['Age'])))
# [{'Age': 30, 'Name': 'Charlie', 'Point': 70, 'State': 'CA'},
# {'Age': 40, 'Name': 'Alice', 'Point': 80, 'State': 'CA'},
# {'Age': 20, 'Name': 'Bob', 'State': 'NY'}]
辞書のリストに対するmax(), min()
冒頭に書いたように、辞書dictに対しては<や>による比較がサポートされていないため、辞書のリストをそのままmax()やmin()の引数に指定するとエラーになる。
l = [{'Name': 'Alice', 'Age': 40, 'Point': 80},
{'Name': 'Bob', 'Age': 20},
{'Name': 'Charlie', 'Age': 30, 'Point': 70}]
# max(l)
# TypeError: '>' not supported between instances of 'dict' and 'dict'
sorted()やsort()と同じく、max()やmin()でも引数keyを指定できる。
print(max(l, key=lambda x: x['Age']))
# {'Name': 'Alice', 'Age': 40, 'Point': 80}
print(min(l, key=lambda x: x['Age']))
# {'Name': 'Bob', 'Age': 20}
辞書オブジェクトが返されるので、値を取り出したい場合はさらにキーを指定すればよい。
print(max(l, key=lambda x: x['Age'])['Age'])
# 40
もちろんoperator.itemgetter()を使ってもよい。
print(max(l, key=operator.itemgetter('Age')))
# {'Name': 'Alice', 'Age': 40, 'Point': 80}