Pythonのソート自由自在 ~初歩から関数を用いた複雑なソートまで~ [reverse, key, lambda, cmp_to_key]

当記事は、ソートを自由自在に使いこなすための方法を、具体例を多く用いて解説した記事です。

外部ライブラリ(NumPy, pandasなど)に関しては扱っていませんのでご了承ください。

目次

sort()とsorted()

Pythonには、数値や文字列を小さい順/大きい順(文字は早い順/遅い順)で並び替える(=ソートする)関数が用意されています。
それが、sort()sorted() です。

sort()はリストそのものを直接ソートする処理で、

(リスト名).sort()

といった形で使います。

一方で、sorted()は元のリストをソートした新しいリストを作る処理で、基本的に、

sorted(リスト名)

といった形で使います。

sort(), sorted()ともに、引数を指定しないと昇順(小さい順)に並び替えられます。sort()・sorted()の引数はreverseとkeyがあり、降順で並べたい時はreverseを、ある条件に沿って並べたいときはkeyを指定します(詳しくは後程解説します)。

下はsort()、sorted()を利用した例です。リストAにsort()を、リストBにsorted()を適用しています。

sort()

A = [2, 4, 5, 3, 1]

A.sort() #A自体をソート
print(A)

# 出力
[1, 2, 3, 4, 5] # ←A Aそのものがソートされている

sorted

B = [2, 4, 5, 3, 1]

C = sorted(B) # Bをソートした新しいリストCを生成
print(C) #新しいリスト
print(B) #元のリスト

# 出力
[1, 2, 3, 4, 5] # ←C Bを基に新しく作られ、ソートされている
[2, 4, 5, 3, 1] # ←B Bそのものは変更されていない

また、二次元リストをソートすることも可能で、デフォルトでは各要素の1番目の要素を基にソートされ、同じだった場合は2番目の要素…というように並び替えられます。

L = [[1, 1], [2, 2], [0, 2], [1, 0], [1, 2], [0, 1], [2, 1]]
L.sort()
print(L)

# 出力
# [[0, 1], [0, 2], [1, 0], [1, 1], [1, 2], [2, 1], [2, 2]]

引数reverseを指定する

sort()、sorted()ともに、reverseという引数を指定することができます。()の中に「reverse=True」と書くことで、降順に並べることができます。(デフォルトではFalseが指定されています。)

L = [3, 5, 4, 2, 1]
L.sort(reverse=True)
print(L)

# 出力
# [5, 4, 3, 2, 1]

引数keyに関数を渡す

sort()関数・sorted()関数には、引数keyに関数を指定し、並び替える基準を設定することができます。
例えば、整数から成るリストを、絶対値(マイナスを外した数)が小さい順にソートしたいとします。その時は、keyとして絶対値を返す関数absを渡します。

L = [-3, 1, 4, -7, 0]
L.sort(key = abs)
print(L)

# 出力
# [0, 1, -3, 4, -7]
# 絶対値は0, 1, 3, 4, 7なので、正しくソートできている

絶対値が小さい順で出力されています。リストのそれぞれの要素がabsの引数として代入され、その返り値を基にソートされている感じです。

そして、この部分には組み込み関数だけでなく、自作の関数を使用することもできます。abs()と全く同じ動作をする関数myfunc()を作って渡してみます。この時、myfuncには1つ引数を用意してください。リストの各要素がその引数に代入され、その返り値をもとにソートが行われます。

def myfunc(x):
    if x >= 0: return x
    return -x

L = [-3, 1, 4, -7, 0]
L.sort(key = myfunc)
print(L)

# 出力
# [0, 1, -3, 4, -7]

また、これらは二次元リストでも利用することができます。たとえば、[[1,2],[3,4]…]のような、2つペアのリストを要素として持つ二次元リストを考えます。 この時、1つ目の要素ではなく2つ目の要素でソートしたいとします。

def myfunc2(x):
    return x[1]

L = [[5, 2], [7, 1], [9, 5], [2, 4]]
L.sort(key = myfunc2)
print(L)

# 出力
# [[7, 1], [5, 2], [2, 4], [9, 5]]
# 2番目の要素は1, 2, 4, 5になっているので正しくソートできている。

lambda式(ラムダ式、無名関数)を使う

とはいえ、def ~: return ~ のような一行で書けるような処理でも関数を作っていると管理が面倒です。そこで、Pythonにはlambda式(ラムダ式)という物が用意されています。lambda式とはいわば「使い捨て」の関数を簡潔に定義できる記法です。名前を付けることも可能ですが、基本付けません。

Pythonでは次のように記述できます。

lambda (関数名) (引数1), (引数2)… : (返り値)

これだけだと分かりにくいですが、実際に先ほどのmyfunc2()をlambda式で書いてみます。

#先ほどのmyfunc2
def myfunc2(x):
    return x[1]

# myfunc2と同じ機能
lambda x: x[1] #←これ

xを受け取って、x[1]をreturnしています。これだけです。
この関数をsortのkeyに入れれば良いのです。

L = [[5, 2], [7, 1], [9, 5], [2, 1]]
#myfunc2だった部分にlambdaを入れる
L.sort(key = lambda x: x[1])
print(L)

# 出力
# [[7, 1], [5, 2], [2, 4], [9, 5]]
# 先ほどと同じ

複数の関数を用いたソート

先ほどでは評価する関数に数値を指定していましたが、もし異なる要素が同じ数値だった場合にはどうなるでしょうか。

例えば、文字数で判断するとき、下のリストに含まれる要素の文字数は全て同じ5文字です。keyに文字数の長さを返すlen関数を渡して実行してみます。

color_list = ["white", "green", "black", "brown"]
color_list.sort(key = len)

# 出力
# ['white', 'green', 'black', 'brown']
# 変化せず

変化しませんでした。Pythonの仕様では、評価の結果が同じなら元の順序を維持するようになっています。(安定性)

もし同じ長さなのであれば、これまでのソート同様、辞書順(アルファベット順)に出力したいとします。

その場合、(一つ目の関数の結果,二つ目の関数の結果)のようなタプルを返す関数をkeyにすると良いです。ソートの順序はまず1つ目の要素で判断され、同点だったら2つ目、それも同点だったら3つ目…のように並び替えられます。
今回は、第二引数にxをそのまま渡せば文字列が評価され昇順に並び替えられます。

color_list = ["white", "green", "black", "brown"]
color_list.sort(key = lambda x: (len(x), x))

また、二次元リストにも適用することができます。例えば、数字のペアのうち1つ目は昇順、2つ目は降順に並べ替えたいとします。
「降順」を表すために、降順にしたい部分が入る所にマイナスをつけます。ソートされたリストの要素は
1<2<3…
のように並んでいますが、全ての要素に-1をかけると
-1>-2>-3…
のようになり大小関係が逆転するからです。

L = [[1,1],[2,2],[0,2],[1,0],[1,2],[0,1],[2,1]]
# x[1]にマイナスが付いている
L.sort(key = lambda x: (x[0], -x[1]))
print(L)

# 結果
# [[0, 2], [0, 1], [1, 2], [1, 1], [1, 0], [2, 2], [2, 1]]

まずタプルの1つ目のx[0]で1つ目の要素が昇順に並び替えられ、1つ目の要素が等しいペア同士が2つ目の-x[1]で降順に並び替えられています。

functools.cmp_to_key()を用いた、要素同士を比較するソート

先ほどの関数では関数の返り値の数値や文字列を基にリストをソートしました。しかし、時には全ての要素を返り値のみで比較することが難しい場合があります。要素を2個の大小が比較できる場合は、functools.cmp_to_key()関数を用いることができます。

手順としては、まず比較するために「x,yを引数として受け取り、yと比べてxが小さいなら-1、等しいなら0、大きいなら1を返す関数」を作ります。そしてfunctools.cmp_to_key(関数名)をソートのキーに渡します。

1つ、設定を考えてみます。

  • 整数に「強さ」があるとします。
  • 偶数は奇数より強く、奇数同士、偶数同士では大きい数のほうが強いとします。

これらの条件を基に、リストの数値を弱い順に並べたいとします。

import functools

# nが偶数ならTrue,奇数ならFalseを返す関数
def iseven(n):
    if n % 2 == 0:
        return True
    return False

# yよりxが小さければ-1、
# 等しいなら0、
# 大きいなら1を返す関数
def myfunc3(x, y):
    # もし、xとyがどちらも偶数、
    # またはどちらも偶数でない(奇数)ならば
    # x、yの大きさで判断
    if iseven(x) == iseven(y):
        if x < y: return -1
        elif x == y: return 0
        else: return 1

    # そうでないなら、x、yの大きさに関わらず偶数の方が強い
    else:
        if iseven(x):
            return 1
        else:
            return -1

L = [1, 2, 3, 4, 6, 6, 99, 100]

# cmp_to_keyの引数にmyfunc3を指定
L.sort(key = functools.cmp_to_key(myfunc3))
print(L)

# 出力
# [1, 3, 99, 2, 4, 6, 6, 100]

cmp_to_key()を用いて並べることができました。

おわりに

ここまで読んでくださりありがとうございました。 誤字、誤っている情報などありましたらコメント欄にお願いします。

参考文献