Bagaimana cara mengurutkan objek dengan banyak kunci dengan Python?

97

Atau, secara praktis, bagaimana saya dapat mengurutkan daftar kamus dengan banyak tombol?

Saya punya daftar penis:

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}]

dan saya perlu menggunakan pengurutan multi-kunci yang dibalik oleh Total_Points, lalu tidak dibalik TOT_PTS_Misc.

Ini dapat dilakukan pada command prompt seperti:

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

Tetapi saya harus menjalankan ini melalui sebuah fungsi, di mana saya meneruskan daftar dan tombol sortir. Misalnya def multikeysort(dict_list, sortkeys):,.

Bagaimana garis lambda dapat digunakan yang akan mengurutkan daftar, untuk sejumlah sembarang kunci yang diteruskan ke fungsi multikeysort, dan mempertimbangkan bahwa kunci sortir mungkin memiliki sejumlah kunci dan kunci yang memerlukan pengurutan terbalik akan diidentifikasi dengan '-' sebelumnya?

simi
sumber

Jawaban:

73

Jawaban ini berfungsi untuk semua jenis kolom dalam kamus - kolom yang dinegasikan tidak harus berupa angka.

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)

Anda bisa menyebutnya seperti ini:

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

Cobalah dengan salah satu kolom dinegasikan. Anda akan melihat urutan sortir terbalik.

Selanjutnya: ubah agar tidak menggunakan extra class ....


2016-01-17

Mengambil inspirasi saya dari jawaban ini Apa cara terbaik untuk mendapatkan item pertama dari kondisi pencocokan yang berulang? , Saya mempersingkat kode:

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)

Jika Anda suka kode Anda singkat.


Kemudian 2016-01-17

Ini berfungsi dengan python3 (yang menghilangkan cmpargumen ke 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))

Terinspirasi oleh jawaban ini Bagaimana saya harus melakukan pengurutan kustom dengan Python 3?

hughdbrown
sumber
Ini bekerja paling baik karena saya dapat menggunakan kebalikannya pada kunci atau kolom apa pun. Terima kasih!
simi
Jadi ini bekerja dengan baik. Saya memanggil fungsi saya dengan daftar dan string sebagai parameter. Saya membagi string terlebih dahulu lalu memanggil multikeysort dengan daftar dan daftar kunci dari string terpisah. Tidak masalah item mana dalam string yang memiliki '-' di awal nama kolom, karena ini akan berfungsi dengan salah satu item atau semua item. Hebat. Terima kasih.
Simi
2
Terima kasih, Anda menyelamatkan hari saya!
Sander van Leeuwen
4
cmp()tidak tersedia untuk Python3, jadi saya harus mendefinisikannya sendiri, seperti yang disebutkan di sini: stackoverflow.com/a/22490617/398514
pferate
8
@hughdbrown: Anda menghapus cmpkata kunci, tetapi cmp()fungsinya masih digunakan 4 baris di atas. Saya mencobanya dengan 3.2, 3.3, 3.4 dan 3.5, semuanya gagal pada pemanggilan fungsi, karena cmp()tidak ditentukan. Poin ketiga di sini ( docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons ) menyebutkan memperlakukan cmp()sebagai hilang.
pferate
57

Artikel ini memiliki ikhtisar yang bagus tentang berbagai teknik untuk melakukan ini. Jika persyaratan Anda lebih sederhana daripada "multikey dua arah penuh", lihat. Jelas jawaban yang diterima dan posting blog yang baru saja saya referensikan mempengaruhi satu sama lain dalam beberapa hal, meskipun saya tidak tahu urutan mana.

Jika tautan mati, inilah sinopsis singkat dari contoh yang tidak tercakup di atas:

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']))
Scott Stafford
sumber
Sedekat yang saya tahu, stygianvision menggunakan kode saya dan tidak memberikan kredit. Google untukresult = cmp(fn(left), fn(right))
hughdbrown
4
Terima kasih atas sinopsisnya, Link sebenarnya sudah mati sekarang. :)
Amyth
49

Saya tahu ini adalah pertanyaan yang agak lama, tetapi tidak ada jawaban yang menyebutkan bahwa Python menjamin urutan yang stabil untuk rutinitas penyortirannya seperti list.sort()dan sorted(), yang berarti item yang dibandingkan sama mempertahankan urutan aslinya.

Artinya padanan ORDER BY name ASC, age DESC(menggunakan notasi SQL) untuk daftar kamus dapat dilakukan seperti ini:

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

Perhatikan bagaimana item pertama kali diurutkan berdasarkan atribut "lebih rendah" age (menurun), kemudian menurut atribut "major" name, yang mengarah ke urutan akhir yang benar.

Pembalikan / pembalik berfungsi untuk semua jenis yang dapat dipesan, tidak hanya angka yang dapat Anda negasikan dengan meletakkan tanda minus di depan.

Dan karena algoritma Timsort digunakan dalam (setidaknya) CPython, ini sebenarnya lebih cepat dalam praktiknya.

guling wouter
sumber
2
sangat bagus. untuk kumpulan data yang moderat di mana menyortir kumpulan beberapa kali tidak menjadi masalah, ini sangat keren! Seperti yang Anda tunjukkan, Anda harus membalik jenis python dibandingkan dengan jenis sql. Terima kasih.
Greg
Urutan kedua akan merusak hasil dari urutan pertama. Lucu bahwa tidak ada pemberi suara yang menyadarinya.
gunung berapi
9
lucu bahwa Anda tidak memperhatikan bahwa kriteria pengurutan utama berada di urutan terakhir, seperti yang ditunjukkan dalam contoh saya, dan secara eksplisit disebutkan di komentar lain untuk membuatnya sangat jelas jika Anda tidak memperhatikan.
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']))
Alex Martelli
sumber
Wow! Itu mengagumkan. Ini bekerja dengan baik. Saya seorang pemula sehingga saya merasa saya tidak akan pernah sampai pada titik mengetahui semua ini. Itu juga cepat. Terima kasih banyak.
simi
Tapi, bagaimana jika kunci yang dikirim ke sortkeypicker adalah string, seperti '-Total_Points, TOT_PTS_Misc'?
simi
1
Kemudian Anda dapat memisahkan string menjadi array terlebih dahulu dengan memanggilsome_string.split(",")
Jason Creighton
Terima kasih. Saya menyadari bahwa saya dapat melakukan split string, setelah saya berkomentar. DOH!
simi
2
Tetapi bagaimana jika Anda meniadakan nilai string alih-alih nilai angka? Saya tidak berpikir itu akan berhasil.
Nick Perkins
5

Saya menggunakan yang berikut ini untuk mengurutkan array 2d pada sejumlah kolom

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

Ini dapat diperpanjang untuk mengerjakan sejumlah item yang berubah-ubah. Saya cenderung berpikir menemukan pola akses yang lebih baik ke kunci yang dapat diurutkan lebih baik daripada menulis pembanding yang mewah.

>>> 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]]
mumrah
sumber
4

Saya memiliki masalah serupa hari ini - saya harus mengurutkan item kamus dengan menurunkan nilai numerik dan dengan nilai string menaik. Untuk mengatasi masalah konflik arah, saya meniadakan nilai integer.

Berikut adalah varian dari solusi saya - yang berlaku untuk OP

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

Sangat sederhana - dan bekerja seperti pesona

[{'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}]
gunung berapi
sumber
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)

Demonstrasi:

>>> 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'}]

Penguraiannya agak rapuh, tetapi setidaknya memungkinkan jumlah variabel spasi di antara kunci.

Torsten Marek
sumber
Tapi, ketika saya memiliki item kedua dalam string dengan '-', itu memberi saya jenis operan yang buruk untuk unary - error.
simi
Anda tidak bisa mengambil negatif dari sebuah string.
Torsten Marek
Ya, saya tahu, tapi beginilah cara parameter diteruskan. Bahkan jika saya melakukan pemisahan, salah satu akan dimulai dengan '-'. Saya pikir kunci sortir perlu dipisahkan sebelum memanggil key_getter, dengan cara itu setiap item dalam daftar kunci akan memeriksa karakter pertama. Apakah saya di jalur yang benar?
simi
0

Karena Anda sudah terbiasa dengan lambda, inilah solusi yang tidak terlalu bertele-tele.

>>> 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)
A. Coady
sumber
Ini tidak bekerja. Saya memiliki: values ​​= ['-Total_Points', 'TOT_PTS_Misc'] lalu b sebagai daftar dicts Ketika saya memanggil g = itemgetter (values) (b) Saya mendapatkan AttributeError: 'list' object tidak memiliki atribut 'startswith'
simi
Dibutuhkan sejumlah nama variabel, bukan daftar nama. Sebut saja seperti ini: itemgetter (* values). Lihat operator bawaan yang serupa.itemgetter untuk contoh lain.
A. Coady