Python'da nesneleri birden çok anahtara göre nasıl sıralayabilirim?


97

Ya da pratik olarak, bir sözlük listesini birden çok tuşa göre nasıl sıralayabilirim?

Bir sözler listem var:

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

ve Total_Points tarafından tersine çevrilen, sonra tarafından tersine çevrilmeyen bir çoklu anahtar sıralaması kullanmam gerekiyor TOT_PTS_Misc.

Bu, aşağıdaki gibi komut isteminde yapılabilir:

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

Ancak bunu, listeyi ve sıralama anahtarlarını geçtiğim bir işlev aracılığıyla çalıştırmam gerekiyor. Örneğin def multikeysort(dict_list, sortkeys):,.

Multikeysort işlevine iletilen keyfi sayıda anahtar için listeyi sıralayacak olan lambda satırı nasıl kullanılabilir ve sıralama anahtarlarının herhangi bir sayıda anahtar içerebileceğini ve ters sıraya ihtiyaç duyanlar tanımlanacaktır. önünde '-' ile?

Yanıtlar:


73

Bu cevap, sözlükteki her tür sütun için işe yarar - reddedilen sütunun bir sayı olması gerekmez.

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)

Şöyle diyebilirsiniz:

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

Her iki sütun da reddedilmiş olarak deneyin. Sıralamanın tersini göreceksiniz.

Sonraki: ekstra sınıf kullanmayacak şekilde değiştirin ....


2016-01-17

Bu cevaptan ilham alıyorum Bir koşulla eşleşen yinelenebilir bir durumdan ilk maddeyi almanın en iyi yolu nedir? , Kodu kısalttım:

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)

Kod özetinizi beğenmeniz durumunda.


Daha sonra 2016-01-17

Bu, python3 ile çalışır ( cmpargümanını ortadan kaldırır 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))

Bu cevaptan ilham aldım Python 3'te özel sıralamayı nasıl yapmalıyım?


Bu en iyi sonucu verir çünkü tersini herhangi bir tuş veya sütunda kullanabilirim. Teşekkür ederim!
simi

Yani bu iyi çalışıyor. İşlevimi liste ve dizeyle parametre olarak çağırıyorum. Önce dizeyi böldüm, sonra multikeysort'u liste ve bölünmüş dizedeki anahtarların listesi ile çağırıyorum. Dizedeki hangi öğenin sütun adının başında '-' işaretine sahip olduğunun önemi yoktur, çünkü bu öğelerin herhangi birinde veya tüm öğelerle çalışacaktır. Harika. Teşekkür ederim.
simi

2
Teşekkürler, günümü kurtardın!
Sander van Leeuwen

4
cmp()Python3 için mevcut değil, bu yüzden burada belirtildiği gibi onu kendim tanımlamak zorunda kaldım: stackoverflow.com/a/22490617/398514
2016'da

8
@hughdbrown: cmpAnahtar kelimeyi kaldırdınız , ancak cmp()işlev hala 4 satır yukarıda kullanılıyor. 3.2, 3.3, 3.4 ve 3.5 ile denedim, hepsi işlev çağrısında başarısız oldu çünkü cmp()tanımlı değil. Buradaki üçüncü madde ( docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons ), cmp()gitmiş gibi davranmaktan bahsediyor .
2016

56

Bu makale , bunu yapmak için çeşitli teknikler hakkında güzel bir özet içeriyor. Gereksinimleriniz "tam çift yönlü çok yönlü" den daha basitse, bir göz atın. Kabul edilen cevap ve az önce başvurduğum blog yazısı, hangi sırayla olduğunu bilmesem de, bir şekilde birbirini etkiledi.

Bağlantının kesilmesi durumunda, yukarıda ele alınmayan örneklerin çok hızlı bir özeti:

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

Anlayabildiğim kadarıyla, stygianvision kodumu kullanıyor ve hiçbir kredi vermiyor. Google forresult = cmp(fn(left), fn(right))
hughdbrown

4
Özet için teşekkürler, Link artık öldü. :)
Amyth

49

Bunun oldukça eski bir soru olduğunu biliyorum, ancak yanıtların hiçbiri Python'un list.sort()ve gibi sıralama rutinleri için kararlı bir sıralama düzenini garanti ettiğinden bahsetmiyor sorted(), bu da eşit karşılaştıran öğelerin orijinal sıralarını koruduğu anlamına geliyor.

Bu ORDER BY name ASC, age DESC, bir sözlük listesi için (SQL gösterimi kullanarak) eşdeğerinin şu şekilde yapılabileceği anlamına gelir :

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

Öğelerin önce "daha az" özniteliğe age(azalan), ardından "büyük" özniteliğe göre namenasıl sıralandığına ve doğru son sıraya götürdüğüne dikkat edin.

Ters çevirme / ters çevirme, sadece önüne bir eksi işareti koyarak reddedebileceğiniz sayılar için değil, tüm sıralanabilir türler için çalışır.

Ve (en azından) CPython'da kullanılan Timsort algoritması nedeniyle, bu aslında pratikte oldukça hızlıdır.


2
çok hoş. kümeyi birden çok kez sıralamanın önemli olmadığı orta düzey veri kümeleri için bu süper harika! Sizin de işaret ettiğiniz gibi, python sıralamasını sql sıralamasına göre tersine çevirmelisiniz. Teşekkürler.
Greg

İkinci sıralama, ilkinin sonucunu bozacaktır. Oy verenlerin hiçbirinin bunu fark etmemesi komik.
volcano

9
benim örneğimde gösterildiği gibi birincil sıralama ölçütünün en son gittiğini fark etmemiş olmanız ve diğer yorumda açıkça belirtmeniz, fark etmemiş olmanız durumunda bunu çok açık hale getirmeniz komik.
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']))

Vaov! Bu mükemmel. Harika çalışıyor. O kadar acemiyim ki tüm bunları bilme noktasına asla gelemeyeceğimi hissediyorum. Bu da hızlıydı. Çok teşekkür ederim.
simi

Ancak, sıralama anahtarı seçiciye gönderilen anahtarlar '-Toplam_points, TOT_PTS_Misc' gibi bir dizeyse ne olur?
simi

1
O zaman öncesome_string.split(",")
Jason Creighton

Teşekkür ederim. Zaten yorum yaptıktan sonra dizeyi bölebileceğimi fark ettim. DOH!
simi

2
Peki ya sayı değeri yerine dize değerini olumsuzlarsanız? Bunun işe yarayacağını sanmıyorum.
Nick Perkins

5

Bir 2d dizisini birkaç sütunda sıralamak için aşağıdakini kullanıyorum

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

Bu, keyfi sayıda öğe üzerinde çalışacak şekilde genişletilebilir. Sıralanabilir anahtarlarınıza daha iyi bir erişim düzeni bulmanın, süslü bir karşılaştırıcı yazmaktan daha iyi olduğunu düşünüyorum.

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

4

Bugün benzer bir sorun yaşadım - sözlük öğelerini azalan sayısal değerlere ve artan dize değerlerine göre sıralamak zorunda kaldım. Çelişkili yönler sorununu çözmek için tamsayı değerlerini reddettim.

İşte çözümümün bir çeşidi - OP için geçerli olduğu şekilde

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

Çok basit - ve bir cazibe gibi çalışıyor

[{'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)

Gösteri:

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

Ayrıştırma biraz kırılgandır, ancak en azından anahtarlar arasında değişken sayıda boşluğa izin verir.


Ancak, dizede '-' olan ikinci öğeye sahip olduğumda, tekli hata için bana kötü bir işlenen türü veriyor.
simi

Bir dizenin negatifini alamazsınız.
Torsten Marek

Evet, biliyorum ama parametreler bu şekilde aktarılıyor. Bölme yapsam bile, biri veya diğeri '-' ile başlayacak. Bence key_getter'ı çağırmadan önce sıralama anahtarlarının bölünmesi gerekiyor, bu şekilde anahtar listesindeki her öğe ilk karakteri kontrol edecek. Doğru yolda mıyım?
simi

0

Lambda konusunda zaten rahat olduğunuz için, işte daha az ayrıntılı bir çözüm.

>>> 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)

Bu çalışmıyor. Elimde: values ​​= ['-Toplam_Points', 'TOT_PTS_Misc'] ve sonra b dicts listesi olarak g = itemgetter (values) (b) AttributeError alıyorum: 'list' nesnesinin 'startswith' özniteliği yok
simi

Bir isim listesi değil, değişken sayıda isim alır. Bunu şöyle adlandırın: itemgetter (* değerler). Başka bir örnek için benzer yerleşik operatör.itemgetter'a bir göz atın.
A. Coady
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.