nikkie-ftnextの日記

イベントレポートや読書メモを発信

温故知新!「ソート HOW TO」で知ったDecorate-Sort-Undecorate(key引数がある今、これを使う必要はないわ)

この記事はPython Advent Calendar 2023 12日目にしちゃいます!先行するんだ

はじめに

そーーーっ nikkieです1

私はPython公式ドキュメントが大好きでしょっちゅう入り浸っています。
そんなドキュメントの中から、PythonのソートについてDecorate-Sort-Undecorateというテクニックを知りました。

目次

まとめ(明日の仕事には役立ちません🙏)

「Decorate-Sort-Undecorate、こんなのあるんだ〜」と気持ちが動いたので執筆しました。
しかしながら、Decorate-Sort-Undecorateの知識自体がふだんのプログラミングで役立つことは皆無だと思います。

  • Decorate-Sort-Undecorateとは
    • ソートしたいリストを、ソートに使う値と、インデックスと、元の値の3要素のタプルからなるリストに変換(Decorate)
    • 変換後のリストを並べ替え(Sort)、元の値を取り出す(Undecorate)
  • key引数に関数を渡すの別解
    • key引数を使いましょう(より短く書けます)
    • key引数にはoperatorを使うと読みやすいぞ

Decorate-Sort-Undecorateなるものがある!

「ソート HOW TO」を読んでいて目が止まりました。
https://docs.python.org/ja/3/howto/sorting.html#decorate-sort-undecorate

私としては学びがあったので取り上げて一記事書きますが、知らなくても問題ありません

いまや Python のソートは key 関数による方法を提供しているので、このテクニックは不要でしょう。

別名「DSUアプローチ」や「Schwartzian transform」と呼ぶようです。

Perl プログラマの間で有名な Randal L. Schwartz にちなんでいます。

3つのステップからなるそうです。

  • まず、元となるリストをソートしたい順序を制御する新しい値でデコレートします。
  • 次に、デコレートしたリストをソートします。
  • 最後に、デコレートを取り除き、新しい順序で元々の値のみを持つリストを作ります。

Decorate -> Sort -> Undecorateの順で3ステップですね。

Decorate-Sort-Undecorate サンプルコード

「ソート HOW TO」で定義した学生データ(Student)をageをキーに昇順でソートします2

Decorateがこちら:

decorated = [
    (student.age, i, student) for i, student in enumerate(student_objects)
]

ageの昇順にしたいので、タプルの第1要素にageを取り出しました。

enumerateして振ったiは、ソートを安定にする効果があります。
たしかにageが同じときiだけが違うので、元々の順番を維持(=安定3)しますね。
もう1つ、Studentオブジェクト同士は比較が未定義(TypeErrorを送出4)ですが、第1・第2要素だけあればデコレートしたタプルは比較できます。
直接比較できないオブジェクトであってもDecorate-Sort-Undecorateでソートできるようにしているわけですね。

Sortは、デコレートしたリストのsortメソッドを呼び出すだけです。

decorated.sort()

Undecorateでデコレートしたリスト(ソート済み)から学生データを取り出します

[student for age, i, student in decorated]

こうしてDecorate-Sort-Undecorateで並べ替えられました!

% python dsu_example.py                
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

今回の動作環境は Python 3.11.4 です。

key関数による方法(オススメ)

「ソート HOW TO」には「Key 関数」のセクションがあります。
https://docs.python.org/ja/3/howto/sorting.html#key-functions
これはlist.sort()sortedのkey引数に渡す関数を指します。

key パラメータの値は関数または呼び出し可能オブジェクトであって、単一の引数をとり、ソートに利用されるキー値を返すものでなければいけません。

key引数には無名関数(lambda)を渡してもよいのですが、「operator モジュール関数」が個人的な推しポイント!
https://docs.python.org/ja/3/howto/sorting.html#operator-module-functions
operatorのitemgetter、attrgetterで無名関数より読みやすく感じるんですよね

以上を使うと、Decorate-Sort-Undecorateと同じ並べ替えがより短いコードでできちゃいます!

from operator import attrgetter

print(sorted(student_objects, key=attrgetter("age")))

終わりに

「ソート HOW TO」よりDecorate-Sort-Undecorateを温ねました。
key引数に関数を渡さなくても、この方法でソートできるわけですね〜。
こういう発見があるので、(今回はふだんのプログラミングに役に立たない例でしたが)ドキュメントを読んでいくのは楽しいなと思います。

DSUはkey引数に代替されていますが、「ソート HOW TO」全体としては非常に役に立つドキュメントでオススメです。

Pythonのドキュメントは広大です(ヨミキレナイヨ〜)。
「ドキュメントで(あんまり役に立たないけど)こんなこと知った!」を共有して、アドベントカレンダーを盛り上げていただいても嬉しいです(私は読みます!)


  1. 二人がいい雰囲気なので
  2. ドキュメント通りのgradeの昇順だと、並び替えなくてもすでにその順番のため例を変えました
  3. ソートの安定についてはこちらもどうぞ
  4. student_objectsの2つの要素を<で比較すると「TypeError: '<' not supported between instances of 'Student' and 'Student'