Programmer's Gate / Effective Python /オブジェクトのソート

1   オブジェクトのソート

オブジェクトをソートするには以下の2通りのどちらかで実現できる。

  1. オブジェクト自身が比較の方法を知っている
  2. オブジェクトの比較方法を表す関数(オブジェクト)を渡す

2.の例を見てみよう。リストのsort()メソッドに比較用の関数(cmpの特性を持つ関数)を渡している。

>>> L = range(5)
>>> L
[0, 1, 2, 3, 4]
>>> L.sort(lambda x, y: cmp(y, x))
>>> L
[4, 3, 2, 1, 0]

cmpの特性を持っていれば、呼び出し可能オブジェクトを渡しても良い。

>>> class AttrCmp:
...     def __init__(self, attr):
...         self.attr = attr
...     def __call__(self, x, y):
...         return cmp(getattr(x, self.attr), getattr(y, self.attr))
...
>>> class Foo:
...     def __init__(self, n, s):
...         self.n, self.s = n, s
...     def __repr__(self):
...         return 'Foo(%d, %s)' % (self.n, self.s)
...
>>> L = [Foo(3, 'b'), Foo(2, 'c'), Foo(1, 'd'), Foo(4, 'a')]
>>> L.sort(AttrCmp('s'))
>>> L
[Foo(4, a), Foo(3, b), Foo(2, c), Foo(1, d)]
>>> L.sort(AttrCmp('n'))
>>> L
[Foo(1, d), Foo(2, c), Foo(3, b), Foo(4, a)]

但し、比較関数を渡す方法は、オブジェクトの比較の度に比較関数が呼び出されるので キーx、yを取り出すのが、それぞれ O(n log(n)) 回かかり、キーの比較自体も O(n log(n)) 回かかる。 以下のように、先に(key, obj)のリストを作りソートすれば キーの取り出しが O(n) 回、比較が O(n log(n)) となり、キーの取り出しに時間がかかる ケースではかなり早くなる。

>>> def sortby(seq, n):
...     seq2 = [(obj[n], obj) for obj in seq]  # decorate
...     seq2.sort()                            # sort
...     return [obj for (key, obj) in seq2]    # undecorate
...
>>> seq = [(1, 'cd'), (2, 'ef'), (3, 'ab')]
>>> sortby(seq, 1)
[(3, 'ab'), (1, 'cd'), (2, 'ef')]

これは、 DecorateSortUndecorate (DSU) と呼ばれているテクニックである。 タプル(リスト)のソートの特性を利用して、(key, obj)のタプルを作り ソートしている。但し、気をつけなければならないのは、第1要素のキーの値が同じ値だった場合に 第2要素が比較されてしまうので、重複のキーが存在する場合は、第2要素にobjのインデックス番号 を追加する必要がある。

2   ネイティブDSU

Python 2.4からsort()メソッドやsorted()関数で、DSUをネイティブにサポートしている。 キーワード引数 key を使用し、キーを取得する関数(オブジェクト)を指定できる。 すると内部でDSUが使用される。使い方は以下の通り。

>>> import operator
>>> seq = [(1, 'cd'), (2, 'ef'), (3, 'ab')]
>>> sorted(seq, key=operator.itemgetter(1))
[(3, 'ab'), (1, 'cd'), (2, 'ef')]

オブジェクトの属性をキーにする事もできる。

>>> import operator
>>> class Foo:
...     def __init__(self, n, s):
...         self.n, self.s = n, s
...     def __repr__(self):
...         return 'Foo(%d, %s)' % (self.n, self.s)
...
>>> L = [Foo(3, 'b'), Foo(2, 'c'), Foo(1, 'd'), Foo(4, 'a')]
>>> L.sort(key=operator.attrgetter('s'))
>>> L
[Foo(4, a), Foo(3, b), Foo(2, c), Foo(1, d)]

つまり、sort()やsorted()に指定する関数(オブジェクト)は、 シーケンスの要素を引数に1つ取り、キーの値を返値とする という特性を持っていれば良い。 以下は、大文字小文字を区別せずに文字列をソートする。

>>> 'This is how to sort words by case insensitive.'.split()
['This', 'is', 'how', 'to', 'sort', 'words', 'by', 'case', 'insensitive.']
>>> sorted(_, key=str.lower)
['by', 'case', 'how', 'insensitive.', 'is', 'sort', 'This', 'to', 'words']

3   安定なソート

list.sortの実装はPythonのバージョンによって変化しているが、Python 2.3以降は 安定(stable) なソートになっている。 安定とは2つの要素を比較して等しかったとき、ソート済みリストの中でオリジナルの 相対位置が保持されるということである。 また仕様上では、Python 2.4以降は安定であることが保証される。

4   ソート方法のまとめ

リストLのソートは以下の3通りで全て解決可能。 ここで、値はリストの要素を表し、キーはソートの比較対象を表す。

  1. 値そのものがキーとなる場合
    • そのままソート可能 => L.sort()もしくは、sorted(L)
  2. 値の中にキーが含まれている場合
    • キーワード引数keyを使用 => L.sort(key=foo)もしくは、sorted(L, key=foo)
    • ここで、fooは値を引数としてキーを返す関数。 operator.itemgetter、operator.attrgetter、lambda式などを使う。
  3. 値の中にキーが含まれていない場合
    • DSUを使用 => [(key1, value1), (key2, value2), ...]を作り(Decorate)、ソートして、Undecorate
    • stableにソートしたい場合は、keyをタプルにして最後の要素にindex番号を入れる => [((k1, 0), value1), ((k2, 1), value2), ...]
    • キーが複数ある場合もkeyをタプルにしてキーの優先順に並べておく。

参考文献:

  1. 『Python Cookbook 2nd Edition』; 5. Searching and Sorting
  2. Sorting Mini-HOWTO (http://www.python.jp/Zope/articles/tips/sorthowto)
  3. Python Performance Tips; 正しいデータ構造を選択する (http://homepage2.nifty.com/turky/PythonSpeed/PerformanceTips.html#id2)
  4. 西尾泰和のブログ; 新入社員3日目日記 (http://www.nishiohirokazu.org/blog/2007/04/3_1.html)
  5. Python ライブラリリファレンス; 3.10 operator -- 関数形式の標準演算子 (http://www.python.jp/doc/2.4/lib/module-operator.html)