[Python]多次元配列のソート

昨今の…

世の中の情勢に伴い、僕の会社でもテレワークを行っています。新入社員も、テレワークでお勉強。いろいろと課題を出しているのですが、いろいろと躓いている様です。その躓き方がなんともお微笑ましくて。

Pythonで多次元配列のソート

一応、大学でPythonを勉強してきた。と言う事だったので、より実践に近い課題を出しています。その中の1つとして、『Excelで作ったみたいな、CSVファイルを読み込んで、任意のカラム(列)で昇順/降順ソートして。』って言うのを出したところ、講師役の先輩社員が『え?』と言い出しました。こっちが『え?』です(^^;)

Googleで簡単に調べたところ、確かに答えに辿り着きそうで、辿り着かないのかも知れないな…と感じました。その位、メジャーじゃ無い。ってことなのでしょうか?

多次元配列

Pythonには、多次元配列っぽい物がいくつかあります。正しい意味合いでは、Array()と言うものが一応あるのですが、一般的なスクリプト言語の様な柔軟性は皆無で、実質使い物にはなりません。
Pythonでは『配列』に相当する型として『リスト』と言う物があります。Python 公式ページに一応リストの扱い方の説明があるのですが、ある程度Pythonを扱える人でないと、あの情報だけでマトモに扱える人はいないかも知れません。(5. データ構造)

上にも書いた様にPythonには、Array()と言う物も存在するため(array — 効率のよい数値アレイ¶)、混乱に落ちる人が多い様です。

取り敢えず、Pythonで『多次元配列』と言うと、『リストのリスト』(『リストのリストのリストの…』)である。と理解しておけばOKです。(因みに、一般的なスクリプト言語で言うところの『ハッシュ』のことを、Pythonでは『辞書型(ディクショナリ)』と呼びます。Python特有の方言みたいな物ですね。)

多次元配列の記述法

他のスクリプト言語同様、var[key1][key2]の用に記述できます。この時、var[key1]とだけ指定すると、key2で指定されるであろう、リスト全体を参照することができます。
この性質はとても重要で、ラムダ式の中で指定する lambda x: x[0] などの x[0] は、多次元配列の1次元目(列)を指定しています。(x は仮変数なので、特別な意味はありません。)
取り敢えず、(恐らく)これだけの知識があれば、多次元配列のソートは(解説を読めば)理解できます。

初期化の方法や、代入のやり方, キーの取り出し方など、詳しい事は Google で調べて頂ければ、適当なページが見つかると思います。

ラムダ式(無名関数)

早速、実例を示したいのですが、その前に、前置きをもうひとつ。

この方法では、ラムダ式(無名関数)と言う物を使用します。ラムダ式は、map, filter と共に難解とされ、正しく理解することが困難ですが、ここでは正しく理解する必要はありません。sort(や sorted)と組み合わせる場合の使い方は一種類しかありません。

むしろ、ラムダ式を理解するための第一歩として、良い勉強になります。

多次元配列(多次元リスト)のソート の実例

と言う事で、Pythonで多次元配列をソートする実例です。この方法以外にも、やり方はあるのですが、多分この方法が一番シンプルです。NumPyのargsort()を使用する方法も紹介される事が多いですが、この方法の方がシンプルです。


#!/usr/bin/python
#-*- coding: utf-8 -*-

import sys

#-----------------------------------------------------------
# sort2.py: 2次元配列(リストのリスト)のソート
#   表計算ソフトなどで作成される、CSVファイル形式の
#   データを、Pythonを使用して、ソートする様なイメージ。
#    ・要素(列)ごとの、優先順位を指定することができる。
#    ・要素(列)ごとに、昇順・降順を指定することができる。
#    ・上記を複数個, 混在することができる。
#
#    ポイント
#    ・ソートに、要素(列)を指定したい場合は、
#      lambda式(無名関数)を使う
#    ・複数の要素(列)を指定したい場合は、
#      lambda式の中身を、括弧で囲み要素番号を書く。
#    ・昇順の場合には、要素番号は素直に書く。
#    ・降順の場合には、仮変数の前にマイナスを付ける。
#
# ここでは、sort() (リスト型のメソッド)を使用しますが、
# sorted(Pythonの組み込み関数)でも、基本的な方法は、
# 同じです。(sortedは、新しいリストに結果が格納される。
# と言う違いがあるだけ。)
#
# ここでは、lambda式の仮変数に、x を使用しているが、
# 別の仮変数名(例えば、sort_para とか)にしても良い。
#   ex) l_2d.sort(key=lambda sort_para: sort_para[1])
#-----------------------------------------------------------


# 2次元配列のダンプ(文字列として返す)
def dump_2d(a):
    s  = "  | 0   1     2    3   4\n"
    s += "--+" + "-" * 21 + "\n"
    for k in range(len(a)):
        s += ("%2d|%2d, %2d, %s, %3d, %2d\n" % \
            (k, a[k][0], a[k][1], a[k][2], a[k][3], a[k][4]))
    return s

# テスト用の2次元配列
#   5列(5要素) × 複数行
l_2d = [[ 4, 30, "あか", 300, 10], [ 2, 30, "しろ", 100, 30],
        [ 9, 20, "あお", 300, 10], [11, 30, "しろ", 300, 20],
        [ 3, 10, "ちゃ", 200, 10], [ 6, 20, "あか", 200, 50],
        [ 8, 30, "ちゃ", 200, 10], [ 1, 20, "しろ", 300, 50],
        [ 5, 30, "あか", 200, 50], [12, 10, "ちゃ", 100, 40],
        [10, 20, "しろ", 200, 20], [ 7, 20, "あお", 100, 40]]

# 結果保存用の、テキストファイルを用意する
outfile = "sort2.txt"
try:
    outf = open(outfile, "w", encoding="utf_8")
except OSError:
    print ("open error: " + outfile + "\n")
else:
    # リストをそのまま表示
    title = "normal"
    s = dump_2d(l_2d)
    print (title + "\n" + s)
    outf.write(title + "\n" + s + "\n")

    # 単純にソートする場合
    #   sorted の場合:
    #       new_l_2d = sorted(l_2d)
    title = "l_2d.sort()"
    l_2d.sort()
    print (title + "\n" + s)
    outf.write(title + "\n" + s + "\n")

    # 第1列(昇順)ソート
    #   sorted の場合:
    #       new_l_2d = sorted(l_2d, key=lambda x: x[1])
    title = "l_2d.sort(key=lambda x: x[1])"
    l_2d.sort(key=lambda x: x[1])
    s = dump_2d(l_2d)
    print (title + "\n" + s)
    outf.write(title + "\n" + s + "\n")

    # 第3列(降順)ソート
    #   sorted の場合:
    #       new_l_2d = sorted(l_2d, key=lambda x: -x[3])
    title = "l_2d.sort(key=lambda x: -x[3])"
    l_2d.sort(key=lambda x: -x[3])
    s = dump_2d(l_2d)
    print (title + "\n" + s)
    outf.write(title + "\n" + s + "\n")

    # 第4列(昇順) > 第1列(昇順) ソート
    #   sorted の場合:
    #       new_l_2d = sorted(l_2d, key=lambda x: (x[2], x[1]))
    title = "l_2d.sort(key=lambda x: (x[4], x[1]))"
    l_2d.sort(key=lambda x: (x[4], x[1]))
    s = dump_2d(l_2d)
    print (title + "\n" + s)
    outf.write(title + "\n" + s + "\n")

    # 第2列(昇順) > 第1列(昇順) > 第3列(昇順) ソート
    #   sorted の場合:
    #       new_l_2d = sorted(l_2d, key=lambda x: (x[2], x[1], x[3]))
    title = "l_2d.sort(key=lambda x: (x[2], x[1], x[3]))"
    l_2d.sort(key=lambda x: (x[2], x[1], x[3]))
    s = dump_2d(l_2d)
    print (title + "\n" + s)
    outf.write(title + "\n" + s + "\n")

    # 第2列(昇順) > 第1列(降順) > 第3列(昇順) ソート
    #   sorted の場合:
    #       new_l_2d = sorted(l_2d, key=lambda x: (x[2], -x[1], x[3]))
    title = "l_2d.sort(key=lambda x: (x[2], -x[1], x[3]))"
    l_2d.sort(key=lambda x: (x[2], -x[1], x[3]))
    s = dump_2d(l_2d)
    print (title + "\n" + s)
    outf.write(title + "\n" + s + "\n")

    # 第2列(昇順) > 第1列(降順) > 第3列(昇順) ソート(sorted使用)
    #   sort の場合:
    #       l_2d.sort(key=lambda x: (x[2], -x[1], x[3]))
    title = "new_l_2d = sorted(l_2d, key=lambda x: (x[2], -x[1], x[3]))"
    new_l_2d = sorted(l_2d, key=lambda x: (x[2], -x[1], x[3]))
    s = dump_2d(new_l_2d)
    print (title + "\n" + s)
    outf.write(title + "\n" + s + "\n")

    # 結果保存用ファイルクローズ
    outf.close()

sys.exit()

これを実行すると、この様な結果が得られます。


normal
  | 0   1     2    3   4
--+---------------------
 0| 4, 30, あか, 300, 10
 1| 2, 30, しろ, 100, 30
 2| 9, 20, あお, 300, 10
 3|11, 30, しろ, 300, 20
 4| 3, 10, ちゃ, 200, 10
 5| 6, 20, あか, 200, 50
 6| 8, 30, ちゃ, 200, 10
 7| 1, 20, しろ, 300, 50
 8| 5, 30, あか, 200, 50
 9|12, 10, ちゃ, 100, 40
10|10, 20, しろ, 200, 20
11| 7, 20, あお, 100, 40

l_2d.sort()
  | 0   1     2    3   4
--+---------------------
 0| 4, 30, あか, 300, 10
 1| 2, 30, しろ, 100, 30
 2| 9, 20, あお, 300, 10
 3|11, 30, しろ, 300, 20
 4| 3, 10, ちゃ, 200, 10
 5| 6, 20, あか, 200, 50
 6| 8, 30, ちゃ, 200, 10
 7| 1, 20, しろ, 300, 50
 8| 5, 30, あか, 200, 50
 9|12, 10, ちゃ, 100, 40
10|10, 20, しろ, 200, 20
11| 7, 20, あお, 100, 40

l_2d.sort(key=lambda x: x[1])
  | 0   1     2    3   4
--+---------------------
 0| 3, 10, ちゃ, 200, 10
 1|12, 10, ちゃ, 100, 40
 2| 1, 20, しろ, 300, 50
 3| 6, 20, あか, 200, 50
 4| 7, 20, あお, 100, 40
 5| 9, 20, あお, 300, 10
 6|10, 20, しろ, 200, 20
 7| 2, 30, しろ, 100, 30
 8| 4, 30, あか, 300, 10
 9| 5, 30, あか, 200, 50
10| 8, 30, ちゃ, 200, 10
11|11, 30, しろ, 300, 20

l_2d.sort(key=lambda x: -x[3])
  | 0   1     2    3   4
--+---------------------
 0| 1, 20, しろ, 300, 50
 1| 9, 20, あお, 300, 10
 2| 4, 30, あか, 300, 10
 3|11, 30, しろ, 300, 20
 4| 3, 10, ちゃ, 200, 10
 5| 6, 20, あか, 200, 50
 6|10, 20, しろ, 200, 20
 7| 5, 30, あか, 200, 50
 8| 8, 30, ちゃ, 200, 10
 9|12, 10, ちゃ, 100, 40
10| 7, 20, あお, 100, 40
11| 2, 30, しろ, 100, 30

l_2d.sort(key=lambda x: (x[4], x[1]))
  | 0   1     2    3   4
--+---------------------
 0| 3, 10, ちゃ, 200, 10
 1| 9, 20, あお, 300, 10
 2| 4, 30, あか, 300, 10
 3| 8, 30, ちゃ, 200, 10
 4|10, 20, しろ, 200, 20
 5|11, 30, しろ, 300, 20
 6| 2, 30, しろ, 100, 30
 7|12, 10, ちゃ, 100, 40
 8| 7, 20, あお, 100, 40
 9| 1, 20, しろ, 300, 50
10| 6, 20, あか, 200, 50
11| 5, 30, あか, 200, 50

l_2d.sort(key=lambda x: (x[2], x[1], x[3]))
  | 0   1     2    3   4
--+---------------------
 0| 7, 20, あお, 100, 40
 1| 9, 20, あお, 300, 10
 2| 6, 20, あか, 200, 50
 3| 5, 30, あか, 200, 50
 4| 4, 30, あか, 300, 10
 5|10, 20, しろ, 200, 20
 6| 1, 20, しろ, 300, 50
 7| 2, 30, しろ, 100, 30
 8|11, 30, しろ, 300, 20
 9|12, 10, ちゃ, 100, 40
10| 3, 10, ちゃ, 200, 10
11| 8, 30, ちゃ, 200, 10

l_2d.sort(key=lambda x: (x[2], -x[1], x[3]))
  | 0   1     2    3   4
--+---------------------
 0| 7, 20, あお, 100, 40
 1| 9, 20, あお, 300, 10
 2| 5, 30, あか, 200, 50
 3| 4, 30, あか, 300, 10
 4| 6, 20, あか, 200, 50
 5| 2, 30, しろ, 100, 30
 6|11, 30, しろ, 300, 20
 7|10, 20, しろ, 200, 20
 8| 1, 20, しろ, 300, 50
 9| 8, 30, ちゃ, 200, 10
10|12, 10, ちゃ, 100, 40
11| 3, 10, ちゃ, 200, 10

new_l_2d = sorted(l_2d, key=lambda x: (x[2], -x[1], x[3]))
  | 0   1     2    3   4
--+---------------------
 0| 7, 20, あお, 100, 40
 1| 9, 20, あお, 300, 10
 2| 5, 30, あか, 200, 50
 3| 4, 30, あか, 300, 10
 4| 6, 20, あか, 200, 50
 5| 2, 30, しろ, 100, 30
 6|11, 30, しろ, 300, 20
 7|10, 20, しろ, 200, 20
 8| 1, 20, しろ, 300, 50
 9| 8, 30, ちゃ, 200, 10
10|12, 10, ちゃ, 100, 40
11| 3, 10, ちゃ, 200, 10

発展型

基本的にはこれで良いのですが、開発の現場では様々なパターンで実行してみる。と言う事を良く行います。そう言う場合には、コマンドライン自体を文字列としてしまい、exit()で実行されてしまう。と言う方法を取る場合もあります。

この例でも、応用することができます。


#!/usr/bin/python
#-*- coding: utf-8 -*-

import sys

#-----------------------------------------------------------
# sort2x.py: sort2.py の発展型
#   実行したいコマンドを文字列とし、リストに登録
#   そのリストをループし、exit() を用いて実行。
#
#   sort / sorted を使用した、多次元配列のソートの
#   詳細は、sort2.py のコメント参照。
#-----------------------------------------------------------


# 2次元配列のダンプ(文字列として返す)
def dump_2d(a):
    s  = "  | 0   1     2    3   4\n"
    s += "--+" + "-" * 21 + "\n"
    for k in range(len(a)):
        s += ("%2d|%2d, %2d, %s, %3d, %2d\n" % \
            (k, a[k][0], a[k][1], a[k][2], a[k][3], a[k][4]))
    return s

# テスト用の2次元配列
#   5列(5要素) × 複数行
l_2d = [[ 4, 30, "あか", 300, 10], [ 2, 30, "しろ", 100, 30],
        [ 9, 20, "あお", 300, 10], [11, 30, "しろ", 300, 20],
        [ 3, 10, "ちゃ", 200, 10], [ 6, 20, "あか", 200, 50],
        [ 8, 30, "ちゃ", 200, 10], [ 1, 20, "しろ", 300, 50],
        [ 5, 30, "あか", 200, 50], [12, 10, "ちゃ", 100, 40],
        [10, 20, "しろ", 200, 20], [ 7, 20, "あお", 100, 40]]

# 実行するコマンド
cmda = ["normal", "l_2d.sort()", "l_2d.sort(key=lambda x: x[1])",
 "l_2d.sort(key=lambda x: -x[3])", 
 "l_2d.sort(key=lambda x: (x[4], x[1]))",
 "l_2d.sort(key=lambda x: (x[2], x[1], x[3]))", 
 "l_2d.sort(key=lambda x: (x[2], -x[1], x[3]))", 
 "new_l_2d = sorted(l_2d, key=lambda x: (x[2], -x[1], x[3]))"]


# 結果保存用の、テキストファイルを用意する
outfile = "sort2x.txt"
try:
    outf = open(outfile, "w", encoding="utf_8")
except OSError:
    print ("open error: " + outfile + "\n")
else:

    # コマンドリストのループ
    for cmd in (cmda):
        # normal 以外なら、exit() を行う
        if cmd != "normal":
            exec(cmd)
        # 結果出力
        s = dump_2d(l_2d)
        print (cmd + "\n" + s)
        outf.write(cmd + "\n" + s + "\n")

    # 結果保存用ファイルクローズ
    outf.close()

sys.exit()

結果は掲載しませんが、全く同一の結果を得ることができます。

sortとsorted論争

Pythonで、ソートを扱うと sort を使うのか sorted を使うのか?と言う話しがでてくる様です。
sort は『リスト型のメソッド』で、sortedは『Pythonの組み込み関数』と言う違いがあります。また、実用的には、sortは元の変数が直接ソートされますが、sortedはソート後の新しい変数を作り出す。と言う違いがあります。

数値をソートしたい場合にはsortを、文字列をソートしたい場合にはsortedを。とか。sortよりsortedの方が動作が早い。とか、いろいろと記述もある様ですが、実例を見て頂いて分かる様に、両者に違いを見出すのはほぼ不可能です。
より、一般的なスクリプト言語に近いのは、sortedですが名前で特殊でイヤと言う人もいます。sortの方がよりPythonっポクて良い。(オブジェクト指向言語っポイ。)と言う人もいます。
どちらでも、多次元配列のソートは可能です。(同様に、多次元辞書のソートも可能です。)

どちらでも、目的に応じ、お好きな方を… かな。って思います。