55
48

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

【Python】自由自在にソートしよう!

Last updated at Posted at 2023-06-19

はじめに

こんにちは、kenです。
業務ではGoを、プライベートではPythonを書いています。
最近、Pythonのソートに関して新たに知ったことがあったので備忘録も兼ねて記事にまとめておこうと思います。私が知らなかっただけでおそらく基礎的なことなので、初心者向けの記事になります。
いきなりその事実について書いてもいいのですが、その前に一度Pythonのソートの基本的な部分についておさらいをしておきます。
もうそんなの知ってるよという方は「4. keyに指定した関数でソート」まで飛ばしてください。

1. 昇順ソート

最も基本的なソートです。
Pythonのソートにはsorted関数とsortメソッドが使われます。
sorted関数は引数のリストをソートした新しいリストを返します(非破壊処理)。一方で、sortメソッドはメソッドが呼び出されたリスト自体を直接ソートします(破壊的処理)。
以降ではsorted関数を用いていくことにしますが、sortメソッドの場合でも扱い方は同じなので必要であれば適宜読みかえてください。

# 1. 昇順ソート
A = [1, 5, 4, 2, 3]
print(sorted(A))
# [1, 2, 3, 4, 5]
print(A)
# [1, 5, 4, 2, 3](A自体はソートされていない)

A.sort()
print(A)
# [1, 2, 3, 4, 5](A自体がソートされている)

2. 降順ソート

reverse=Trueとすることで、降順ソートが簡単にできます。

# 2. 降順ソート
A = [1, 5, 4, 2, 3]
print(sorted(A, reverse=True)) # [5, 4, 3, 2, 1]

3. 多次元配列のソート

多次元配列に対してソートをかけた場合は各リストの先頭の要素から順に比較が行われます、2次元配列の場合に例を見てみましょう。

# 3. 多次元配列のソート
A = [[1, 9, 2], [1, 4, 3], [5, 8, 7], [5, 8, 3], [5, 2, 9]]
print(sorted(A))
# [[1, 4, 3], [1, 9, 2], [5, 2, 9], [5, 8, 3], [5, 8, 7]]

ここでは、各リストの最初の要素がまず比較されています(例えば、[1, 9, 2][1, 4, 3]の場合、最初の要素は両方とも1)。もし最初の要素が等しい場合、その次の要素で比較が行われます([1, 9, 2][1, 4, 3]の場合、次の要素は9と4で、これにより[1, 4, 3][1, 9, 2]より小さくなります)。
このことはPythonの公式ドキュメントに次のように記載されています(日本語訳は私によるもの)

Sequences of the same type also support comparisons. In particular, tuples and lists are compared lexicographically by comparing corresponding elements.
(同じ型の配列は比較もサポートしています。特にタプルとリストは、対応する要素を比較することで辞書式順序で比較されます)

もちろんreverse = Trueを指定すれば降順にソートもできます。

A = [[1, 9, 2], [1, 4, 3], [5, 8, 7], [5, 8, 3], [5, 2, 9]]
print(sorted(A, reverse=True))
# [[5, 8, 7], [5, 8, 3], [5, 2, 9], [1, 9, 2], [1, 4, 3]]

デフォルトだと先述したように先頭の要素から順に比較されますが、keyパラメーターを使うことで比較する順番や尺度を変えることができます。
たとえば次のようなカスタマイズが可能です。

A = [[1, 9, 2], [1, 4, 3], [5, 8, 7], [5, 8, 3], [5, 2, 9]]

# 各リストの1番目の要素で昇順ソートする
print(sorted(A, key=lambda x: x[1]))
# 出力: [[5, 2, 9], [1, 4, 3], [5, 8, 7], [5, 8, 3], [1, 9, 2]]

# 各リストの0番目の要素で昇順ソートし、0番目の要素が同じ場合は1番目の要素で降順にソートする
print(sorted(A, key=lambda x: (x[0], -x[1])))
# 出力: [[1, 9, 2], [1, 4, 3], [5, 8, 7], [5, 8, 3], [5, 2, 9]]

# 各リストの先頭の要素から順に比較する(keyパラメーターを指定しないデフォルトのソートのときと一緒)
print(sorted(A, key=lambda x: (x[0], x[1], x[2])))
# 出力: [[1, 4, 3], [1, 9, 2], [5, 2, 9], [5, 8, 3], [5, 8, 7]]

keyパラメーターに指定しているlambda x: x[1]のような記述はラムダ関数(無名関数)と呼ばれるものです。簡単にいうと関数の宣言を簡略化したものでlambda x: x[1]は次のような関数と一緒です。

def func(x): return x[1]

つまり、keyパラメーターには、比較の基準となる値を返す関数を指定すればよいわけです。

あれ、ということはもうちょっと面白いことができそうじゃないですか……??
ということでここからが最近私が知ったこと(冒頭で予告したもの)です。

4. keyに指定した関数でソート

keyパラメーターには、比較する基準となる値を返す関数を渡す」ということは無名関数以外もkeyパラメーターに渡せるということですよね。
実際Pythonのソートに関して説明している公式のドキュメントには次のような記載があります。

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

たとえば、mathモジュールのsin関数を指定すれば、各要素xに対してmath.sin(x)の値に基づいて昇順にソートできます。

import math

pi = math.pi
A = [0, pi / 6, pi / 4, pi / 2, 3 * pi / 4, 5 * pi / 6, pi]
print(sorted(A, key=math.sin))
# 出力: [0, 3.141592653589793, 0.5235987755982988, 2.6179938779914944, 0.7853981633974483, 2.356194490192345, 1.5707963267948966]

出力を見やすくすると
$[0,\pi,\frac{\pi}{6},\frac{5\pi}{6},\frac{\pi}{4},\frac{3\pi}{4},\frac{\pi}{2}]$ です。確かにsin関数に入れたときに値が小さい順に並んでいます。

次の例はstringのindexメソッドを用いた例です。

python_random = ["o", "t", "n", "p", "h", "y"]
print(sorted(python_random, key="python".index))
# 出力: ['p', 'y', 't', 'h', 'o', 'n']

indexメソッドは引数にとった文字がレシーバーであるstringの中で何番目にあるかを返すメソッドです。(たとえば"python".index("y") = 1)
これをkeyパラメーターに指定することでindexメソッドの値が小さい順、つまり"python"の順にpython_randomがソートされるというわけです。

ほとんど同じような例ですが、リストをレシーバーにしたindex関数をもちいれば次のようなこともできます。

ranking = ["Sato", "Suzuki", "Takahashi", "Tanaka", "Ito"]
names = ["Suzuki", "Ito", "Takahashi", "Sato", "Tanaka"]
print(sorted(names, key=ranking.index))
# ['Sato', 'Suzuki', 'Takahashi', 'Tanaka', 'Ito']

これは日本に多い苗字を順に並べたリスト(ranking)を用いて、適当に並べられたnamesというリストをその順番に並べ替えるという操作を実現したものです。

他にはこんな使い方もあるでしょう。

studentsToScores = {
    "Taro": 100,
    "Emi": 95,
    "Hanako": 90,
    "Takeshi": 85,
    "Jiro": 80,
}


def ToScore(student: str):
    if student not in studentsToScores.keys():
        return -1
    return studentsToScores[student]


students = ["Taro", "Hanako", "Jiro", "Emi", "Takeshi", "Jim"]
print(sorted(students, key=ToScore, reverse=True))
# ['Taro', 'Emi', 'Hanako', 'Takeshi', 'Jiro', 'Jim']

上の例はクラスの名簿表をテストの点数に基づいて並び替えるというシチュエーションを想定したものです。
名前をkeyにしてテストの点数をvalueにもつdict(studentsToScores)を用意し、引数にとった生徒の名前に対してその生徒の点数を出力する関数(ToScore)をつくることでテストの点数を基準にソートをすることが可能になります。

おわりに

多次元配列に対してソートをするときkeyパラメーターに無名関数を用いると柔軟にソートができることは知っていたのですが、keyパラメーターに指定する関数によってリスト外の数値を基準にソートすることもできることは知らなかったです…。私が思いついてないだけで、まだまだ上手な使い方を考えられそうですね!

ここまで読んでいただきありがとうございました、間違い等ありましたらコメントにてご指摘ください。

おまけ

import random

def myRandom(n: int):
    return random.random()

numbers = [1, 2, 3, 4, 5]
for _ in range(5):
    print(sorted(numbers, key=myRandom))

randomモジュールのrandom関数をkeyパラメーターに指定すれば比較するのに用いる値が各要素ごとにランダムに決まるので、リストをシャッフルすることができます。まあrandomモジュールにはシャッフル関数(random.shuffle(numbers))があるので使用機会はないと思いますが…。1

出力
[3, 1, 4, 2, 5]
[2, 1, 5, 4, 3]
[2, 4, 5, 1, 3]
[1, 2, 3, 4, 5]
[1, 3, 2, 4, 5]
  1. 先述したようにkeyパラメーターに渡せるのは単一引数の関数なので自作の関数でラップしてます

55
48
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
48