2009-11-01 7 views
5

J'ai une liste de tuples que j'essaie de trier et pourrait utiliser de l'aide.Tri de l'aide: d'abord par cela, puis par cela

Le champ que je veux trier par dans les tuples ressemble à "XXX_YYY". Tout d'abord, je veux grouper les valeurs XXX dans l'ordre inverse, puis, dans ces groupes, je veux placer les valeurs YYY dans l'ordre de tri normal. (NOTE: Je suis tout aussi heureux, en fait, trier le deuxième élément du tuple de cette manière, l'ordre inverse premier mot, ordre normal seconde.)

Voici un exemple de ce que j'ai et ce Je voudrais à la fin ... pas sûr de savoir comment le faire.

mylist = [ 
    (u'community_news', u'Community: News & Information'), 
    (u'kf_video', u'KF: Video'), 
    (u'community_video', u'Community: Video'), 
    (u'kf_news', u'KF: News & Information'), 
    (u'kf_magazine', u'KF: Magazine') 
] 

Je voudrais faire une sorte de sort() sur cette liste qui changera la sortie:

sorted = [ 
    (u'kf_magazine', u'KF: Magazine'), 
    (u'kf_news', u'KF: News & Information'), 
    (u'kf_video', u'KF: Video'), 
    (u'community_news', u'Community: News & Information'), 
    (u'community_video', u'Community: Video'), 
] 

Je pense qu'il peut y avoir un moyen pythonique pour gérer cela, mais ne suis pas en mesure de conclure ma tête l'entoure.

Répondre

8

sur mesure comparaison fonctions de tri, comme suggéré dans les réponses existantes, ne le rendent facile à trier dans un mélange de croissant et les ordres décroissants - mais ils ont de sérieux problèmes de performance et ont été supprimés en Python 3, ne laissant que l'approche de personnalisation préférée - fonctions d'extraction de clé fonctions ... beaucoup plus rapide, bien que plus délicate à utiliser pour le cas d'utilisation relativement rare de mixte types ascendants/descendants.

En Python 2.*, qui prend en charge type de personnalisation (non tant dans le même appel à sort ou sorted :-), une fonction de comparaison personnalisée peut être passé comme un argument nommé cmp=; ou, une fonction d'extraction de clé personnalisée peut être transmise en tant qu'argument nommé key=. En Python 3.*, seule la dernière option est disponible. Cela vaut la peine de comprendre l'approche de l'extraction de clés, même si vous pensez que vous venez de résoudre votre problème avec une approche de comparaison personnalisée: non seulement pour la performance, mais aussi pour la pérennité (Python 3) et pour la généralité (L'approche key= s'applique également à min, max, itertools.groupby ... beaucoup plus générale que l'approche cmp=!).

L'extraction de clé est très simple lorsque tous les sous-champs clés doivent être triés de la même manière (tous ascendants ou tous descendants) - vous venez de les extraire; c'est quand même assez facile si les sous-champs qui vont "dans l'autre sens" sont des nombres (vous changez juste leur signe en extrayant); le cas délicat est exactement celui que vous avez - plusieurs champs de cordes qui doivent être comparés de manière opposée.

Une assez simple approche pour résoudre votre problème est une petite classe shim:

class Reverser(object): 
    def __init__(self, s): self.s = s 
    def __lt__(self, other): return other.s < self.s 
    def __eq__(self, other): return other.s == self.s 

Notez que vous suffit de fournir __lt__ et __eq__ (les opérateurs < et ==) - sort et les amis synthétisent tous les autres comparaisons, si nécessaire, sur la base de ces deux.

Armé de ce petit outil auxiliaire, on peut procéder facilement ...:

def getkey(tup): 
    a, b = tup[0].split('_') 
    return Reverser(a), b 

my_list.sort(key=getkey) 

Comme vous le voyez, une fois que vous « get » et les concepts de l'inverseur d'extraction clés, vous payez essentiellement pas de prix pour en utilisant l'extraction de la clé au lieu de la comparaison personnalisée: le code que je suggère est 4 instructions pour la classe inverseur (que vous pouvez écrire une fois et placée dans votre module "goodies bag" quelque part), trois pour la fonction d'extraction sort ou sorted appel - un total de huit vs les 4 + 1 == 5 de l'approche de comparaison personnalisée dans la forme la plus compacte (c.-à-d. Celle utilisant soit cmp avec un changement de signe, ou cmp avec argume permuté nts). Trois déclarations ne coûtent pas trop cher pour les avantages de l'extraction de clés! -)

La performance n'est clairement pas un gros problème avec une telle liste restreinte, mais avec une durée légèrement plus longue (10 fois) ...:

# my_list as in the Q, my_cmp as per top A, getkey as here 

def bycmp(): 
    return sorted(my_list*10, cmp=my_cmp) 

def bykey(): 
    return sorted(my_list*10, key=getkey) 

... 

$ python -mtimeit -s'import so' 'so.bykey()' 
1000 loops, best of 3: 548 usec per loop 
$ python -mtimeit -s'import so' 'so.bycmp()' 
1000 loops, best of 3: 995 usec per loop 

Ie, la key= approche montre déjà un gain de performance de près de deux fois (trier la liste deux fois plus rapide) lorsque vous travaillez sur une liste de 50 articles - vaut bien le prix modeste de « 8 lignes plutôt de 5 ", en particulier avec tous les autres avantages que j'ai déjà mentionnés!

+0

Wow, j'aime ta solution. Je ne savais pas que l'approche cmp = avait une telle pénalité. –

+0

@Steven, tx-yep, tout le monde ne comprend pas pourquoi cmp = a été supprimé dans Python 3 (comme une "nuisance attrayante" qui tente les gens à souffrir d'une pénalité de performance!), C'est pourquoi j'ai posté cette explication détaillée, merci pour confirmer cela peut aider! -) –

+2

@Alex: J'hésite à modifier l'une de * vos * réponses mais peut-être que my_list.key (cmp = my_cmp) devrait être my_list.sort (clé = getkey)? –

10
def my_cmp(x, y): 
    x1, x2 = x[0].split('_') 
    y1, y2 = y[0].split('_') 
    return -cmp(x1, y1) or cmp(x2, y2) 

my_list = [ 
    (u'community_news', u'Community: News & Information'), 
    (u'kf_video', u'KF: Video'), 
    (u'community_video', u'Community: Video'), 
    (u'kf_news', u'KF: News & Information'), 
    (u'kf_magazine', u'KF: Magazine') 
] 

sorted_list = [ 
    (u'kf_magazine', u'KF: Magazine'), 
    (u'kf_news', u'KF: News & Information'), 
    (u'kf_video', u'KF: Video'), 
    (u'community_news', u'Community: News & Information'), 
    (u'community_video', u'Community: Video'), 
] 

my_list.sort(cmp=my_cmp) 
assert my_list == sorted_list 
+1

J'étais sur le point d'éditer le mien pour annuler l'appel cmp à la place quand vous avez posté votre réponse. :) – Kylotan

+1

J'ai encore simplifié la comparaison à '-cmp (x1, y1) ou cmp (x2, y2)'. :) –

+0

vous pouvez également passer l'argument clé pour trier et se débarrasser de la scission en haut de votre fonction: my_list.sort (cmp = my_cmp, clé = lambda x: x [0] .split ('_')) –

2
>>> def my_cmp(tuple_1, tuple_2): 
    xxx_1, yyy_1 = tuple_1[0].split('_') 
    xxx_2, yyy_2 = tuple_2[0].split('_') 
    if xxx_1 > xxx_2: 
     return -1 
    elif xxx_1 < xxx_2: 
     return 1 
    else: 
     return cmp(yyy_1, yyy_2) 


>>> import pprint 
>>> pprint.pprint(sorted(mylist, my_cmp)) 
[(u'kf_magazine', u'KF: Magazine'), 
(u'kf_news', u'KF: News & Information'), 
(u'kf_video', u'KF: Video'), 
(u'community_news', u'Community: News & Information'), 
(u'community_video', u'Community: Video')] 

Pas la plus jolie solution dans le monde ...