Pythonで複数のキーでオブジェクトを並べ替える方法


96

または、実際には、複数のキーで辞書のリストをどのようにソートできますか?

辞書のリストがあります:

b = [{u'TOT_PTS_Misc': u'Utley, Alex', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Russo, Brandon', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Chappell, Justin', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Foster, Toney', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Lawson, Roman', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Lempke, Sam', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Gnezda, Alex', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Kirks, Damien', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Worden, Tom', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Korecz, Mike', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Swartz, Brian', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Burgess, Randy', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Smugala, Ryan', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Harmon, Gary', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Blasinsky, Scott', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Carter III, Laymon', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Coleman, Johnathan', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Venditti, Nick', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Blackwell, Devon', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Kovach, Alex', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Bolden, Antonio', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Smith, Ryan', u'Total_Points': 60.0}]

そして、Total_Pointsによって逆転され、次にによって逆転されないマルチキーソートを使用する必要がありますTOT_PTS_Misc

これは、コマンドプロンプトで次のように実行できます。

a = sorted(b, key=lambda d: (-d['Total_Points'], d['TOT_PTS_Misc']))

しかし、これを関数で実行する必要があります。この関数では、リストと並べ替えキーを渡します。たとえば、def multikeysort(dict_list, sortkeys):

リストをソートするラムダラインをどのように使用して、マルチキーソート関数に渡される任意の数のキーについて、ソートキーに任意の数のキーを含めることができ、逆のソートが必要なキーが識別されることを考慮に入れることができますか?その前に「-」を付けますか?

回答:


72

この回答は、辞書のどの種類の列でも機能します。否定された列は数値である必要はありません。

def multikeysort(items, columns):
    from operator import itemgetter
    comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else
                  (itemgetter(col.strip()), 1)) for col in columns]
    def comparer(left, right):
        for fn, mult in comparers:
            result = cmp(fn(left), fn(right))
            if result:
                return mult * result
        else:
            return 0
    return sorted(items, cmp=comparer)

次のように呼び出すことができます。

b = [{u'TOT_PTS_Misc': u'Utley, Alex', u'Total_Points': 96.0},
     {u'TOT_PTS_Misc': u'Russo, Brandon', u'Total_Points': 96.0},
     {u'TOT_PTS_Misc': u'Chappell, Justin', u'Total_Points': 96.0},
     {u'TOT_PTS_Misc': u'Foster, Toney', u'Total_Points': 80.0},
     {u'TOT_PTS_Misc': u'Lawson, Roman', u'Total_Points': 80.0},
     {u'TOT_PTS_Misc': u'Lempke, Sam', u'Total_Points': 80.0},
     {u'TOT_PTS_Misc': u'Gnezda, Alex', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Kirks, Damien', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Worden, Tom', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Korecz, Mike', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Swartz, Brian', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Burgess, Randy', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Smugala, Ryan', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Harmon, Gary', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Blasinsky, Scott', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Carter III, Laymon', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Coleman, Johnathan', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Venditti, Nick', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Blackwell, Devon', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Kovach, Alex', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Bolden, Antonio', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Smith, Ryan', u'Total_Points': 60.0}]

a = multikeysort(b, ['-Total_Points', 'TOT_PTS_Misc'])
for item in a:
    print item

どちらかの列を否定して試してください。ソート順が逆になります。

次に、余分なクラスを使用しないように変更します。


2016-01-17

この回答からインスピレーションを得て、条件に一致するイテラブルから最初のアイテムを取得する最良の方法は何ですか?、私はコードを短くしました:

from operator import itemgetter as i

def multikeysort(items, columns):
    comparers = [
        ((i(col[1:].strip()), -1) if col.startswith('-') else (i(col.strip()), 1))
        for col in columns
    ]
    def comparer(left, right):
        comparer_iter = (
            cmp(fn(left), fn(right)) * mult
            for fn, mult in comparers
        )
        return next((result for result in comparer_iter if result), 0)
    return sorted(items, cmp=comparer)

あなたがコードを簡潔にしたい場合のために。


その後2016-01-17

これはpython3で動作します(これによりへのcmp引数がなくなりましたsort):

from operator import itemgetter as i
from functools import cmp_to_key

def cmp(x, y):
    """
    Replacement for built-in function cmp that was removed in Python 3

    Compare the two objects x and y and return an integer according to
    the outcome. The return value is negative if x < y, zero if x == y
    and strictly positive if x > y.

    https://portingguide.readthedocs.io/en/latest/comparisons.html#the-cmp-function
    """

    return (x > y) - (x < y)

def multikeysort(items, columns):
    comparers = [
        ((i(col[1:].strip()), -1) if col.startswith('-') else (i(col.strip()), 1))
        for col in columns
    ]
    def comparer(left, right):
        comparer_iter = (
            cmp(fn(left), fn(right)) * mult
            for fn, mult in comparers
        )
        return next((result for result in comparer_iter if result), 0)
    return sorted(items, key=cmp_to_key(comparer))

この回答に触発されましたPython 3でカスタムソートをどのように行う必要がありますか?


これは、任意のキーまたは列で逆を使用できるため、最適に機能します。ありがとうございました!
simi 2009

したがって、これはうまく機能します。リストと文字列をパラメータとして関数を呼び出します。最初に文字列を分割してから、分割した文字列のリストとキーのリストを使用してmultikeysortを呼び出します。文字列のどの項目でも列名の先頭に「-」が付いていてもかまいません。これは、項目またはすべての項目で機能するためです。驚くばかり。ありがとうございました。
simi 2009

2
ありがとう、あなたは私の日を救った!
Sander van Leeuwen、2014年

4
cmp()私はここに述べたように、それを自分自身を定義する必要がありましたので、のpython3では使用できません:stackoverflow.com/a/22490617/398514は
pferate

8
@hughdbrown:cmpキーワードを削除しましたが、cmp()関数は上記の4行でまだ使用されています。3.2、3.3、3.4、3.5で試してみましたcmp()が、が定義されていないため、関数呼び出しですべて失敗しました。ここの3番目の箇条書き(docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons)は、存在しないものcmp()として扱うことについて述べています。
2016

53

この記事には、これを行うためのさまざまな手法の概要が記載されています。要件が「完全な双方向マルチキー」よりも単純な場合は、ご覧ください。受け入れられた回答と私が参照したばかりのブログ投稿は、何らかの順序でお互いに影響を与えたのは明らかですが、どちらの順序かわかりません。

リンクが切れた場合は、上記で取り上げられていない例の非常に簡単な概要を示します。

mylist = sorted(mylist, key=itemgetter('name', 'age'))
mylist = sorted(mylist, key=lambda k: (k['name'].lower(), k['age']))
mylist = sorted(mylist, key=lambda k: (k['name'].lower(), -k['age']))

私の知る限り、stygianvisionは私のコードを使用しており、クレジットを提供していません。Google forresult = cmp(fn(left), fn(right))
hughdbrown 2014

4
あらすじのおかげで、リンクは実際に死んでいます。:)
Amyth

47

私は、これはかなり古い質問ですけど、答えのどれもPythonがそのソートなどのルーチンのための安定したソート順序を保証することを言及していないlist.sort()sorted()同等の元の順序を保持比較項目を意味し、。

これはORDER BY name ASC, age DESC、辞書のリストと同等の(SQL表記を使用して)次のように実行できることを意味します。

items.sort(key=operator.itemgetter('age'), reverse=True)
items.sort(key=operator.itemgetter('name'))

アイテムが最初に「小さい」属性age(降順)で並べ替えられ、次に「メジャー」属性nameで並べ替えられ、正しい最終的な順序になっていることに注意してください。

反転/反転は、マイナス記号を前に置くことで否定できる数値だけでなく、すべての順序付け可能なタイプで機能します。

(少なくとも)CPythonで使用されているTimsortアルゴリズムのため、実際にはこれはかなり高速です。


2
非常に素晴らしい。セットを複数回ソートする必要がない中程度のデータセットの場合、これは非常に便利です。ご指摘のとおり、Pythonの並べ替えをSQLの並べ替えとは逆にする必要があります。ありがとう。
グレッグ

2番目の並べ替えは、最初の並べ替えの結果を壊します。面白いことに、賛成者の誰もそれに気づかなかった。
火山

9
面白いことに、私の例に示すように、プライマリソート基準が最後になることに気づかず、他のコメントで明示的に言及して、気付かなかった場合にそれを明確にします。
wouter bolsterlee

24
def sortkeypicker(keynames):
    negate = set()
    for i, k in enumerate(keynames):
        if k[:1] == '-':
            keynames[i] = k[1:]
            negate.add(k[1:])
    def getit(adict):
       composite = [adict[k] for k in keynames]
       for i, (k, v) in enumerate(zip(keynames, composite)):
           if k in negate:
               composite[i] = -v
       return composite
    return getit

a = sorted(b, key=sortkeypicker(['-Total_Points', 'TOT_PTS_Misc']))

うわー!すごいです。それは素晴らしい働きをします。私はこのような初心者なので、これをすべて知ることはできません。それも速かった。どうもありがとうございました。
simi 2009

しかし、sortkeypickerに送信されるキーが「-Total_Points、TOT_PTS_Misc」のように文字列である場合はどうなりますか?
simi 2009

1
次に、最初に次の呼び出しを使用して文字列を配列に分割できますsome_string.split(",")
Jason Creighton

ありがとうございました。すでにコメントした後で、文字列を分割できることに気づきました。DOH!
simi 2009

2
しかし、数値ではなく文字列値を否定するとどうなるでしょうか。私はそれがうまくいくとは思いません。
Nick Perkins、

5

私はいくつかの列で2D配列をソートするために以下を使用します

def k(a,b):
    def _k(item):
        return (item[a],item[b])
    return _k

これは、任意の数のアイテムで機能するように拡張できます。私は、ソート可能なキーへのより良いアクセスパターンを見つけることは、凝ったコンパレータを書くよりも良いと思う傾向があります。

>>> data = [[0,1,2,3,4],[0,2,3,4,5],[1,0,2,3,4]]
>>> sorted(data, key=k(0,1))
[[0, 1, 2, 3, 4], [0, 2, 3, 4, 5], [1, 0, 2, 3, 4]]
>>> sorted(data, key=k(1,0))
[[1, 0, 2, 3, 4], [0, 1, 2, 3, 4], [0, 2, 3, 4, 5]]
>>> sorted(a, key=k(2,0))
[[0, 1, 2, 3, 4], [1, 0, 2, 3, 4], [0, 2, 3, 4, 5]]

2

今日も同様の問題がありました。数値を降順で、文字列値を昇順で、辞書項目を並べ替える必要がありました。矛盾する方向の問題を解決するために、整数値を無効にしました。

ここに私のソリューションのバリアントがあります-OPに適用可能

sorted(b, key=lambda e: (-e['Total_Points'], e['TOT_PTS_Misc']))

非常にシンプル-魅力のように機能します

[{'TOT_PTS_Misc': 'Chappell, Justin', 'Total_Points': 96.0},
 {'TOT_PTS_Misc': 'Russo, Brandon', 'Total_Points': 96.0},
 {'TOT_PTS_Misc': 'Utley, Alex', 'Total_Points': 96.0},
 {'TOT_PTS_Misc': 'Foster, Toney', 'Total_Points': 80.0},
 {'TOT_PTS_Misc': 'Lawson, Roman', 'Total_Points': 80.0},
 {'TOT_PTS_Misc': 'Lempke, Sam', 'Total_Points': 80.0},
 {'TOT_PTS_Misc': 'Gnezda, Alex', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Kirks, Damien', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Korecz, Mike', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Worden, Tom', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Burgess, Randy', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Harmon, Gary', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Smugala, Ryan', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Swartz, Brian', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Blackwell, Devon', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Blasinsky, Scott', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Bolden, Antonio', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Carter III, Laymon', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Coleman, Johnathan', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Kovach, Alex', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Smith, Ryan', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Venditti, Nick', 'Total_Points': 60.0}]

0
from operator import itemgetter
from functools import partial

def _neg_itemgetter(key, d):
    return -d[key]

def key_getter(key_expr):
    keys = key_expr.split(",")
    getters = []
    for k in keys:
        k = k.strip()
        if k.startswith("-"):
           getters.append(partial(_neg_itemgetter, k[1:]))
        else:
           getters.append(itemgetter(k))

    def keyfunc(dct):
        return [kg(dct) for kg in getters]

    return keyfunc

def multikeysort(dict_list, sortkeys):
    return sorted(dict_list, key = key_getter(sortkeys)

デモンストレーション:

>>> multikeysort([{u'TOT_PTS_Misc': u'Utley, Alex', u'Total_Points': 60.0},
                 {u'TOT_PTS_Misc': u'Russo, Brandon', u'Total_Points': 96.0}, 
                 {u'TOT_PTS_Misc': u'Chappell, Justin', u'Total_Points': 96.0}],
                "-Total_Points,TOT_PTS_Misc")
[{u'Total_Points': 96.0, u'TOT_PTS_Misc': u'Chappell, Justin'}, 
 {u'Total_Points': 96.0, u'TOT_PTS_Misc': u'Russo, Brandon'}, 
 {u'Total_Points': 60.0, u'TOT_PTS_Misc': u'Utley, Alex'}]

解析は少し壊れやすいですが、少なくともキー間のスペースの可変数を可能にします。


しかし、文字列の2番目の項目に '-'が含まれていると、単項エラーのオペランドタイプが不正になります。
simi 2009

文字列の負の値を取得することはできません。
Torsten Marek、

はい、わかりますが、これはパラメーターが渡される方法です。分割しても、どちらかが「-」で始まります。key_getterを呼び出す前にソートキーを分割する必要があると思います。これにより、キーリストの各項目が最初の文字をチェックします。私は正しい軌道に乗っていますか?
simi 2009

0

あなたはすでにラムダに慣れているので、これはそれほど冗長ではないソリューションです。

>>> def itemgetter(*names):
    return lambda mapping: tuple(-mapping[name[1:]] if name.startswith('-') else mapping[name] for name in names)

>>> itemgetter('a', '-b')({'a': 1, 'b': 2})
(1, -2)

これは動作しません。私は次のようにしています:values = ['-Total_Points'、 'TOT_PTS_Misc'] then b as a list of dicts g when call g = itemgetter(values)(b)I AttributeError: 'list' object has no attribute 'startswith'
simi

名前のリストではなく、可変数の名前を取ります。次のように呼び出します:itemgetter(* values)。別の例については、同様の組み込みoperator.itemgetterをご覧ください。
A. Coady
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.