📘

超小ネタ Python 昇順、降順のソートをシンプルに実装したい

に公開

Pythonで辞書型の値が配列にまとまっているときに、ライブラリを使わずソートをする方法をまとめました。
文字列が含まれているパターンのソートに少し悩んだので備忘として残しておきます。

テストデータ

import pprint

test_data = [
  {
    'order_date':    '2024年7月1日',
    'order_number':  'A00001',
    'item_name':     'ふわふわパン3号',
    'item_quantity': 1
  },
  {
    'order_date':    '2024年7月1日',
    'order_number':  'A00002',
    'item_name':     'ふわふわパン1号',
    'item_quantity': 3
  },
  {
    'order_date':    '2024年7月1日',
    'order_number':  'A00003',
    'item_name':     'ふわふわパン2号',
    'item_quantity': 2
  },
  {
    'order_date':    '2024年7月2日',
    'order_number':  'A00004',
    'item_name':     'ふんわりマフィン5号',
    'item_quantity': 5
  },
  {
    'order_date':    '2024年7月2日',
    'order_number':  'A00005',
    'item_name':     'ふんわりマフィン4号',
    'item_quantity': 4
  }
]

基本的なソート方法

基本的なソート方法

文字列の項目だけで単純にソート

昇順

sorted_data = sorted(test_data, key=lambda x: x['item_name'])
pprint.pprint(sorted_data)

item_nameの昇順

[{'item_name': 'ふわふわパン1号',
  'item_quantity': 3,
  'order_date': '2024年7月1日',
  'order_number': 'A00002'},
 {'item_name': 'ふわふわパン2号',
  'item_quantity': 2,
  'order_date': '2024年7月1日',
  'order_number': 'A00003'},
 {'item_name': 'ふわふわパン3号',
  'item_quantity': 1,
  'order_date': '2024年7月1日',
  'order_number': 'A00001'},
 {'item_name': 'ふんわりマフィン4号',
  'item_quantity': 4,
  'order_date': '2024年7月2日',
  'order_number': 'A00005'},
 {'item_name': 'ふんわりマフィン5号',
  'item_quantity': 5,
  'order_date': '2024年7月2日',
  'order_number': 'A00004'}]

降順

sorted_data = sorted(test_data, key=lambda x: x['item_name'], reverse=True)
pprint.pprint(sorted_data)

item_nameの降順

[{'item_name': 'ふんわりマフィン5号',
  'item_quantity': 5,
  'order_date': '2024年7月2日',
  'order_number': 'A00004'},
 {'item_name': 'ふんわりマフィン4号',
  'item_quantity': 4,
  'order_date': '2024年7月2日',
  'order_number': 'A00005'},
 {'item_name': 'ふわふわパン3号',
  'item_quantity': 1,
  'order_date': '2024年7月1日',
  'order_number': 'A00001'},
 {'item_name': 'ふわふわパン2号',
  'item_quantity': 2,
  'order_date': '2024年7月1日',
  'order_number': 'A00003'},
 {'item_name': 'ふわふわパン1号',
  'item_quantity': 3,
  'order_date': '2024年7月1日',
  'order_number': 'A00002'}]

文字列を含む複数項目でソート

昇順

sorted_data = sorted(test_data, key=lambda x: (x['order_date'], x['item_name']))
pprint.pprint(sorted_data)

order_dateの昇順、item_nameの昇順

[{'item_name': 'ふわふわパン1号',
  'item_quantity': 3,
  'order_date': '2024年7月1日',
  'order_number': 'A00002'},
 {'item_name': 'ふわふわパン2号',
  'item_quantity': 2,
  'order_date': '2024年7月1日',
  'order_number': 'A00003'},
 {'item_name': 'ふわふわパン3号',
  'item_quantity': 1,
  'order_date': '2024年7月1日',
  'order_number': 'A00001'},
 {'item_name': 'ふんわりマフィン4号',
  'item_quantity': 4,
  'order_date': '2024年7月2日',
  'order_number': 'A00005'},
 {'item_name': 'ふんわりマフィン5号',
  'item_quantity': 5,
  'order_date': '2024年7月2日',
  'order_number': 'A00004'}]

降順

sorted_data = sorted(test_data, key=lambda x: (x['order_date'], x['item_name']), reverse=True)
pprint.pprint(sorted_data)

order_dateの降順、item_nameの降順

[{'item_name': 'ふんわりマフィン5号',
  'item_quantity': 5,
  'order_date': '2024年7月2日',
  'order_number': 'A00004'},
 {'item_name': 'ふんわりマフィン4号',
  'item_quantity': 4,
  'order_date': '2024年7月2日',
  'order_number': 'A00005'},
 {'item_name': 'ふわふわパン3号',
  'item_quantity': 1,
  'order_date': '2024年7月1日',
  'order_number': 'A00001'},
 {'item_name': 'ふわふわパン2号',
  'item_quantity': 2,
  'order_date': '2024年7月1日',
  'order_number': 'A00003'},
 {'item_name': 'ふわふわパン1号',
  'item_quantity': 3,
  'order_date': '2024年7月1日',
  'order_number': 'A00002'}]

文字列を含む複数項目の各項目で昇順降順を切り替えてソート

この方法が分からなくて悩んだ

整数の項目であれば-(マイナス)を付けて降順にできる

文字列であってもreverse=Trueを付ければ降順にできる

しかし、文字列の場合かつ文字列以外の項目も含めたソートをしたい場合はどうすればいいのか

というわけで以下の関数を追加して挑戦してみる

# 文字列を1文字ずつord関数にかけて、その文字のUnicodeコードポイントを整数として取得
# 降順にしたいので取得した整数に-(マイナス)を付けて、配列にする
def to_ascii_list_reverse(s):
    return [-ord(char) for char in s]

昇順、降順が混在

sorted_data = sorted(test_data, key=lambda x: (x['order_date'], to_ascii_list_reverse(x['item_name'])))
pprint.pprint(sorted_data)

order_dateの昇順、item_nameの降順

[{'item_name': 'ふわふわパン3号',
  'item_quantity': 1,
  'order_date': '2024年7月1日',
  'order_number': 'A00001'},
 {'item_name': 'ふわふわパン2号',
  'item_quantity': 2,
  'order_date': '2024年7月1日',
  'order_number': 'A00003'},
 {'item_name': 'ふわふわパン1号',
  'item_quantity': 3,
  'order_date': '2024年7月1日',
  'order_number': 'A00002'},
 {'item_name': 'ふんわりマフィン5号',
  'item_quantity': 5,
  'order_date': '2024年7月2日',
  'order_number': 'A00004'},
 {'item_name': 'ふんわりマフィン4号',
  'item_quantity': 4,
  'order_date': '2024年7月2日',
  'order_number': 'A00005'}]

一見、正しくソートされているように見えるが、sortedと同じ挙動になるのかテストデータを増やして試してみた

import pprint

def to_ascii_list_reverse(s):
    return [-ord(char) for char in s]

strings = ["ぶどう酒", "いちじくゼリー", "バナナスプリット", "デーツ", "りんごジュース", "チェリーパイ", "さくらんぼ", "ばなな", 
           "だて", "りんご", "いちじく", "ぶどう", "cherry", "fig", "date", "grape", "apple", "banana", "&*()_", 
           "+-*/=", "cherry789", "grape789", "fig456", "banana456", "date123", "apple123", "!@#$%"]

# to_ascii_list_reverseを使って降順にソート
sorted_by_ascii_list_reverse = sorted(strings, key=to_ascii_list_reverse)

# 通常のsortedを使って降順にソート
sorted_by_default_reverse = sorted(strings, reverse=True)

print("to_ascii_list_reverseを使った降順ソート:")
pprint.pprint(sorted_by_ascii_list_reverse)

print("\n通常のsortedを使った降順ソート:")
pprint.pprint(sorted_by_default_reverse)
to_ascii_list_reverseを使った降順ソート:
['バナナスプリット',
 'デーツ',
 'チェリーパイ',
 'りんご',
 'りんごジュース',
 'ぶどう',
 'ぶどう酒',
 'ばなな',
 'だて',
 'さくらんぼ',
 'いちじく',
 'いちじくゼリー',
 'grape',
 'grape789',
 'fig',
 'fig456',
 'date',
 'date123',
 'cherry',
 'cherry789',
 'banana',
 'banana456',
 'apple',
 'apple123',
 '+-*/=',
 '&*()_',
 '!@#$%']

通常のsortedを使った降順ソート:
['バナナスプリット',
 'デーツ',
 'チェリーパイ',
 'りんごジュース',
 'りんご',
 'ぶどう酒',
 'ぶどう',
 'ばなな',
 'だて',
 'さくらんぼ',
 'いちじくゼリー',
 'いちじく',
 'grape789',
 'grape',
 'fig456',
 'fig',
 'date123',
 'date',
 'cherry789',
 'cherry',
 'banana456',
 'banana',
 'apple123',
 'apple',
 '+-*/=',
 '&*()_',
 '!@#$%']

微妙に異なる・・・やはり素人がソートのロジックに首を突っ込むのは骨が折れそう

いきなり結論

答えはココに
https://docs.python.org/ja/3/howto/sorting.html#sort-stability-and-complex-sorts

ソートは、 安定 (stable) であることが保証されています。これはレコードの中に同じキーがある場合、元々の順序が維持されるということを意味します。

つまり、ソートしたい項目を逆順でsorted関数にかけてあげればいいみたい

order_dateの昇順、item_nameの降順なら先にitem_nameの降順にして次にorder_dateの昇順にする

sorted_data = sorted(test_data, key=lambda x: (x['item_name']), reverse=True)
sorted_data = sorted(sorted_data, key=lambda x: (x['order_date']))
pprint.pprint(sorted_data)
[{'item_name': 'ふわふわパン3号',
  'item_quantity': 1,
  'order_date': '2024年7月1日',
  'order_number': 'A00001'},
 {'item_name': 'ふわふわパン2号',
  'item_quantity': 2,
  'order_date': '2024年7月1日',
  'order_number': 'A00003'},
 {'item_name': 'ふわふわパン1号',
  'item_quantity': 3,
  'order_date': '2024年7月1日',
  'order_number': 'A00002'},
 {'item_name': 'ふんわりマフィン5号',
  'item_quantity': 5,
  'order_date': '2024年7月2日',
  'order_number': 'A00004'},
 {'item_name': 'ふんわりマフィン4号',
  'item_quantity': 4,
  'order_date': '2024年7月2日',
  'order_number': 'A00005'}]

良さそう、自分で変なソートロジックを組まなくてよくなった

まとめ

  • ソート条件の項目がそんなに多くなく、ソート対象のデータもそこまで多くないのであれば、何度かsortedを使って、期待するソート順を実現するのも良さそう
  • 複雑化しそうであれば、比較関数を使ったほうが良さそう

参考

参考にさせていただきました、ありがとうございます!

https://dev.classmethod.jp/articles/python-data-sorting/

SMARTCAMP Engineer Blog

Discussion