ソート HOW TO

著者

Andrew Dalke and Raymond Hettinger

リリース

0.1

Python のリストにはリストをインプレースに変更する、組み込みメソッド list.sort() があります。他にもイテラブルからソートしたリストを作成する組み込み関数 sorted() があります。

このドキュメントでは Python を使った様々なソートのテクニックを探索します。

ソートの基本

単純な昇順のソートはとても簡単です: sorted() 関数を呼ぶだけです。そうすれば、新たにソートされたリストが返されます:

>>> sorted([5, 2, 3, 1, 4])
[1, 2, 3, 4, 5]

list.sort() メソッドを呼びだしても同じことができます。この方法はリストをインプレースに変更します (そして sorted との混乱を避けるため None を返します)。多くの場合、こちらの方法は sorted() と比べると不便です - ただし、元々のリストが不要な場合には、わずかですがより効率的です。

>>> a = [5, 2, 3, 1, 4]
>>> a.sort()
>>> a
[1, 2, 3, 4, 5]

違いは他にもあります、 list.sort() メソッドはリストにのみ定義されています。一方 sorted() 関数は任意のイテラブルを受け付けます。

>>> sorted({1: 'D', 2: 'B', 3: 'B', 4: 'E', 5: 'A'})
[1, 2, 3, 4, 5]

Key 関数

list.sort()sorted() には key パラメータがあります。 これは比較を行う前にリストの各要素に対して呼び出される関数 (または呼び出し可能オブジェクト) を指定するパラメータです。

例えば、大文字小文字を区別しない文字列比較の例:

>>> sorted("This is a test string from Andrew".split(), key=str.lower)
['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']

key パラメータの値は関数または呼び出し可能オブジェクトであって、単一の引数をとり、ソートに利用されるキー値を返すものでなければいけません。この制約によりソートを高速に行えます、キー関数は各入力レコードに対してきっちり一回だけ呼び出されるからです。

よくある利用パターンはいくつかの要素から成る対象をインデクスのどれかをキーとしてソートすることです。例えば:

>>> student_tuples = [
...     ('john', 'A', 15),
...     ('jane', 'B', 12),
...     ('dave', 'B', 10),
... ]
>>> sorted(student_tuples, key=lambda student: student[2])   # sort by age
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

同じテクニックは名前づけされた属性 (named attributes) を使うことでオブジェクトに対しても動作します。例えば:

>>> class Student:
...     def __init__(self, name, grade, age):
...         self.name = name
...         self.grade = grade
...         self.age = age
...     def __repr__(self):
...         return repr((self.name, self.grade, self.age))

>>> student_objects = [
...     Student('john', 'A', 15),
...     Student('jane', 'B', 12),
...     Student('dave', 'B', 10),
... ]
>>> sorted(student_objects, key=lambda student: student.age)   # sort by age
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

operator モジュール関数

上述した key 関数のパターンはとても一般的です、 そのため、Python は高速で扱いやすいアクセサ関数を提供しています。 operator モジュールには itemgetter(), attrgetter() そして methodcaller() 関数があります。

これらの関数を利用すると、上の例はもっと簡単で高速になります:

>>> from operator import itemgetter, attrgetter

>>> sorted(student_tuples, key=itemgetter(2))
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

>>> sorted(student_objects, key=attrgetter('age'))
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

operator モジュールの関数は複数の段階でのソートを可能にします。例えば、 grade でソートしてさらに age でソートする場合:

>>> sorted(student_tuples, key=itemgetter(1,2))
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]

>>> sorted(student_objects, key=attrgetter('grade', 'age'))
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]

昇順と降順

list.sort()sorted() の両方とも reverse パラメータを 真偽値として受け付けます。このパラメータは降順ソートを行うかどうかの フラグとして利用されます。 例えば、学生のデータを age の逆順で得たい場合:

>>> sorted(student_tuples, key=itemgetter(2), reverse=True)
[('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]

>>> sorted(student_objects, key=attrgetter('age'), reverse=True)
[('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]

ソートの安定性と複合的なソート

ソートは、 安定 (stable) であることが保証されています。これはレコードの中に同じキーがある場合、元々の順序が維持されるということを意味します。

>>> data = [('red', 1), ('blue', 1), ('red', 2), ('blue', 2)]
>>> sorted(data, key=itemgetter(0))
[('blue', 1), ('blue', 2), ('red', 1), ('red', 2)]

二つの blue のレコードが元々の順序を維持して、 ('blue', 1)('blue', 2) の前にあること注意してください。

この素晴しい性質によって複数のソートを段階的に組み合わせることができます。例えば、学生データを grade の降順にソートし、さらに age の昇順にソートしたい場合には、まず age でソートし、次に grade でもう一度ソートします:

>>> s = sorted(student_objects, key=attrgetter('age'))     # sort on secondary key
>>> sorted(s, key=attrgetter('grade'), reverse=True)       # now sort on primary key, descending
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

この処理は、リストおよびフィールド名とソート順序のタプルを複数受け取れるラッパー関数へ抽象化できます。

>>> def multisort(xs, specs):
...     for key, reverse in reversed(specs):
...         xs.sort(key=attrgetter(key), reverse=reverse)
...     return xs

>>> multisort(list(student_objects), (('grade', True), ('age', False)))
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

Python では Timsort アルゴリズムが利用されていて、効率良く複数のソートを行うことができます、これは現在のデータセット中のあらゆる順序をそのまま利用できるからです。

Decorate-Sort-Undecorate

このイディオムは以下の3つのステップにちなんでデコレート-ソート-アンデコレート (Decorate-Sort-Undecorate) と呼ばれています:

  • まず、元となるリストをソートしたい順序を制御する新しい値でデコレートします。

  • 次に、デコレートしたリストをソートします。

  • 最後に、デコレートを取り除き、新しい順序で元々の値のみを持つリストを作ります。

例えば、DSU アプローチを利用して学生データを grade でソートする場合:

>>> decorated = [(student.grade, i, student) for i, student in enumerate(student_objects)]
>>> decorated.sort()
>>> [student for grade, i, student in decorated]               # undecorate
[('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]

このイディオムはタブルが辞書編集的に比較されるため正しく動作します; 最初の要素が比較され、同じ場合には第二の要素が比較され、以下も同様に動きます。

デコレートしたリストのインデクス i は全ての場合で含まれる必要はありませんが、そうすることで二つの利点があります:

  • ソートが安定になります -- もし二つの要素が同じキーを持つ場合、それらの順序がソートされたリストでも維持されます。

  • 元々の要素が比較可能な要素を持つとは限りません、なぜならデコレートされたタブルの順序は多くの場合、最初の二つの要素で決定されるからです。例として元のリストは直接比較できない複素数を含むことができます。

このイディオムの別名に Schwartzian transform があります。これは Perl プログラマの間で有名な Randal L. Schwartz にちなんでいます。

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

Comparison Functions

Unlike key functions that return an absolute value for sorting, a comparison function computes the relative ordering for two inputs.

For example, a balance scale compares two samples giving a relative ordering: lighter, equal, or heavier. Likewise, a comparison function such as cmp(a, b) will return a negative value for less-than, zero if the inputs are equal, or a positive value for greater-than.

It is common to encounter comparison functions when translating algorithms from other languages. Also, some libraries provide comparison functions as part of their API. For example, locale.strcoll() is a comparison function.

To accommodate those situations, Python provides functools.cmp_to_key to wrap the comparison function to make it usable as a key function:

sorted(words, key=cmp_to_key(strcoll))  # locale-aware sort order

残りのはしばし

  • For locale aware sorting, use locale.strxfrm() for a key function or locale.strcoll() for a comparison function. This is necessary because "alphabetical" sort orderings can vary across cultures even if the underlying alphabet is the same.

  • reverse パラメータはソートの安定性を保ちます (ですから、レコードのキーが等しい場合元々の順序が維持されます)。面白いことにこの影響はパラメータ無しで reversed() 関数を二回使うことで模倣することができます:

    >>> data = [('red', 1), ('blue', 1), ('red', 2), ('blue', 2)]
    >>> standard_way = sorted(data, key=itemgetter(0), reverse=True)
    >>> double_reversed = list(reversed(sorted(reversed(data), key=itemgetter(0))))
    >>> assert standard_way == double_reversed
    >>> standard_way
    [('red', 1), ('red', 2), ('blue', 1), ('blue', 2)]
    
  • ソートルーチンは二つのオブジェクトを比較するのに < を利用することを保証しています。そのため __lt__() メソッドを定義することで、標準のソート順序を追加できます:

    >>> Student.__lt__ = lambda self, other: self.age < other.age
    >>> sorted(student_objects)
    [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
    

    However, note that < can fall back to using __gt__() if __lt__() is not implemented (see object.__lt__()).

  • key 関数はソートするオブジェクトに依存する必要はありません。 key 関数は外部リソースにアクセスすることもできます。例えば学生の成績が辞書に保存されている場合、それを利用して別の学生の名前のリストをソートすることができます:

    >>> students = ['dave', 'john', 'jane']
    >>> newgrades = {'john': 'F', 'jane':'A', 'dave': 'C'}
    >>> sorted(students, key=newgrades.__getitem__)
    ['jane', 'dave', 'john']