動機
Pythonにはソート機能が用意されていて、list.sort()メソッドやsorted()関数があります。
自作のクラスに関するソートについては調べてみたのですが、
これだ!というものが中々見つからなかったので備忘録として残しておきます。
やり方
公式によりますと、
Key関数というものを指定することで行えるとなっています。
また、それ以外でもクラスの比較演算子オーバーロードでも行えるようです。
比較演算子での例
勉強がてらトランプゲームを作っていたのですが、
トランプクラスからどうやってソートをすればいいのかというところで、
最初は比較演算子のオーバーロードを考えました。(C++プログラマ並感)
こんなスートと数値を持つトランプクラスがあったとして
(コードは一部省略しています)
class Trump:
def __init__(self, suit, number):
self.suit = suit
self.number = number
トランプクラスに比較演算子のオーバーロードを定義します。
def __lt__(self, other):
"""比較演算子<"""
return self.get_sort_number() < other.get_sort_number()
def __gt__(self, other):
"""比較演算子>"""
return self.get_sort_number() > other.get_sort_number()
get_sort_number()というのは並べる順番をintの数値の大きさで定義したものです。
小さい方が先頭にくるようなイメージです。
スート的にはスペード、ハート、ダイヤモンド、クラブ、ジョーカーの順番で並びます。
get_sort_number()上ではスートの数値(Trump.SORT_〜)と数字を足すことで、
ソートするための数値を取得しています。
def get_sort_number(self):
"""ソート用の整数を取得する"""
n = 0
if self.suit == Trump.SPADE:
n = Trump.SORT_SPADE
elif self.suit == Trump.HEART:
n = Trump.SORT_HEART
elif self.suit == Trump.DIAMOND:
n = Trump.SORT_DIAMOND
elif self.suit == Trump.CLUB:
n = Trump.SORT_CLUB
elif self.suit == Trump.JOKER:
n = Trump.SORT_JOKER
# ソートと数値を加味した数値を返す
return n + self.number
ちなみにソート数値定義をのせておきます
各スートごとの数値は13なので20にする必要はありませんが、キリよく20離しています。
スペードの13なら
get_sort_number()で取れる数値は 0 + 13で13
ハートの1なら
get_sort_number()で取れる数値は 20 + 1で21
となります。
この数値の大きさでソートをするわけです。
SORT_SPADE = 0
SORT_HEART = 20
SORT_DIAMOND = 40
SORT_CLUB = 60
SORT_JOKER = 80
このトランプ達をまとめるTrumpHandクラスを定義します。(厳密には必要ないですが)
単純にTrumpクラスをlistで持つだけですね。
ソートするときはlistのsortメソッドを呼ぶだけで大丈夫です。
勝手に昇順に並び替えてくれます。
class TrumpHand:
def __init__(self):
self.hand = []
def sort(self):
"""手札をソートする"""
self.hand.sort()
これを実行してみましょう
(addやprintの説明は省略しています 名前だけで想像つきますよね)
# 手札クラスを生成(単純にlistで持っているだけです)
hand = trump_hand.TrumpHand()
# ジョーカーとクラブの1とスペードの1を手札に加えます
hand.add(trump.Trump(trump.Trump.JOKER, 1))
hand.add(trump.Trump(trump.Trump.CLUB, 1))
hand.add(trump.Trump(trump.Trump.SPADE, 1))
# ソート前の状態を出力
hand.print()
# ソート
hand.sort()
# ソート後の状態を出力
hand.print()
ソート前の出力です。
J1(ジョーカー)、C1(クラブの1)、S1(スペードの1)と上のコードで追加した順番で入っています。
[0]J1
[1]C1
[2]S1
ソート後の出力です。
S1(スペードの1)、C1(クラブの1)、J1(ジョーカー)
とget_sort_number()の説明で言ってた通りの順番になっています。
[0]S1
[1]C1
[2]J1
問題点
これでめでたしめでたし……とはなりませんでした。
問題点1
トランプの比較演算子をソートの順番に使っていいのか?
比較演算子はトランプの強さの比較に使う用途も考えられます。
問題点2
ゲームのルールによっては条件によって強さが変わったり、
もしくはユーザーが見やすいようにして欲しいと思うので、
固定では融通がききにくいのではないでしょうか?
もう一つの方法
ここでようやくKey関数を使ったソートの出番です。
公式に既に書かれているのでこの記事を見なくてもいいっちゃいい
ただ私はすぐ理解できなかったんだもん
公式より引用
list.sort() と sorted() には key パラメータがあります。 これは比較を行う前にリストの各要素に対して呼び出される関数を指定するパラメータです。
keyパラメータは単一の引数をとり、ソートに利用される key を返さなければいけません。この制約によりソートを高速に行えます、キー関数は各入力レコードに対してきっちり一回だけ呼び出されるからです。
やり方
Key関数にソートに使用する関数を指定すればいいというのはわかったと思います。
問題はKey関数をどうやって作ればいいかですが、
公式をよくみると
よくある利用パターンはいくつかの要素から成る対象をインデクスのどれかをキーとしてソートすることです。
これは今回で言うget_sort_number()がこれにあたりそうです。
スートと数字の要素から成るキーを返す、それをソートしてもらう、そのままですね。
昇順の並び替えなら数値が低ければ先頭、高ければ後尾に並ぶはずです。
と言うことでコードを書き換えます。
(注)こちらの方法より下に書かれているtrump.Trump.get_sort_numberを指定する方法を推奨します
書き換え前
def sort(self):
"""手札をソートする"""
self.hand.sort()
書き換え後(ラムダ式)
def sort(self):
"""手札をソートする"""
self.hand.sort(key=lambda x: x.get_sort_number())
sortメソッドの呼び出しの際に key= でKey関数を指定します。
今回はソート対象(Trumpクラス)のget_sort_number()を返してソートに利用してもらいます。
補足としてxはTrumpクラスですね。
また、上で書いた比較演算子のオーバーロードは不要です、消しておきましょう。
ラムダ式で書いていますが、普通に書くならこんな感じでしょうか。(未検証)
# どこかにKey関数定義
def get_sort_key(x):
return x.get_sort_number()
# sortの書き換え(Key関数を指定)
def sort(self):
"""手札をソートする"""
self.hand.sort(key=get_sort_key)
推奨方法
2020/04/19 18時追記
@shiracamus さんよりこちらの方がプログラム意図が明確なので推奨します(動作検証済み)
コメントありがとうございましたー
書き換え前
def sort(self):
"""手札をソートする"""
self.hand.sort()
書き換え後(key関数にtrump.Trump.get_sort_numberを指定)
def sort(self):
"""手札をソートする"""
self.hand.sort(key=trump.Trump.get_sort_number)
Key関数を指定したバージョンを実行するとこうなります。
ソート前の出力
[0]J1
[1]C1
[2]S1
ソート後の出力
[0]S1
[1]C1
[2]J1
はい、比較演算子と同じ出力になっています。
状況や仕様に応じてKey関数の指定を変えれば柔軟にソートできるようになりますね。
懺悔
Pythonは数日前に始めたばかりなので間違っている箇所があったらすみません
Trumpクラスはビット演算で1つの変数に入れることができますが分かりやすさ優先で
get_sort_number()もっと短く書けるよね