PROJET AUTOBLOG


Sam et Max

source: Sam et Max

⇐ retour index

Ordonner en Python

lundi 21 janvier 2013 à 14:37

Python possède une manière de mettre les choses dans l’ordre qui est à la fois simple et puissante.

Tout est ordonnable

Pour comprendre comment marche le tri en Python, il faut comprendre que presque tout en Python est ordonnable car comparable :

>>> 1 > 0 # les nombres sont comparables
True
>>> 1 < 0
False
>>> "a" < "b" # les lettres, par ordre alphabetique
True
>>> True > False # les booléan (!)
True
>>> (1, 2) > (2, 1) # les iterables comparés, élément par élément dans l'ordre
False
>>> (1, 2) > [2, 1] # mais ils doivent être du même type
True
>>> {1:1} < {1:1, 0:0} # les dictionnaires, par nombre d'éléments
True
>>> "a" > 2 # si on mélange des types ça peut vide devenir bizarre
True
>>> 1j > 1 # j'ai dis PRESQUE tout est ordonnable
Traceback (most recent call last):
  File "<ipython-input-11-ed3c013d3df8>", line 1, in <module>
    1j > 1
TypeError: no ordering relation is defined for complex numbers

C’est ce qu’on appelle l’ordre naturel des éléments. Quand il n’y a pas d’ordre naturel évident (et que ce n’est pas une opération explicitement interdite comme avec les complexes), Python va comparer l’id (imaginez l’adresse en mémoire):

>>> class PersonnageDeLost(object):
...     pass
... 
>>> mort1 = PersonnageDeLost()
>>> mort2 = PersonnageDeLost()
>>> mort1 < mort2
True
>>> id(mort1) # son id est plus petit, donc il est inférieur
39611408
>>> id(mort2)
41720976

Mais on peut définir un ordre naturel pour un élément personnalisé en implémentant les méthodes suivantes:

N’implémentez plus __cmp__, elle est dépréciée.

Par exemple:

class PersonnageDeCityHunter(object):
 
    def __init__(self, nom, erectopouvoir):
        self.nom = nom
        self.erectopouvoir = erectopouvoir
 
 
    def __lt__(self, other):
        # on doit retourner un booléan qui confirme ou infirme 
        # l'opération "lt" ("lower than", c'est à dire "plus petit que")
        return self.erectopouvoir < other.erectopouvoir
 
    def __gt__(self, other):
        # on doit retourner un booléan qui confirme ou infirme 
        # l'opération "gt" ("greater than", c'est à dire "plus grand que")
        return self.erectopouvoir > other.erectopouvoir
 
    # on peut faire la même chose pour les autres méthodes qui 
    # concernent les opérateurs <=, >=, == et !=
 
>>> PersonnageDeCityHunter('Ryo Saeba', 99999) > PersonnageDeCityHunter('Mamouth', 1)
True
>>> PersonnageDeCityHunter('Ryo Saeba', 99999) < PersonnageDeCityHunter('Mamouth', 1)
False

Ordonner une liste

Une liste est ce qu’on ordonne le plus souvent. Elle possède pour cela une méthode sort() qui est la méthode de tri la plus rapide qui existe avec la lib standard.

sort() trie une liste sur place (elle modifie donc la liste), et retourne None.

>>> l = [1, 7, 3, 8]
>>> res = l.sort()
>>> print res # n'assignez jamais le résultat de sort(), ça ne sert à rien !
None
>>> l
[1, 3, 7, 8]

Les éléments sont triés dans leur ordre naturel automatiquement, du plus petit au plus grand:

>>> l = ['b', 'a', 'c']
>>> l.sort()
>>> l
['a', 'b', 'c']
>>> l = [(2, 1), (1, 2), (7, 8), (2, 2), (2, 0), (2, 3)]
>>> l.sort()
>>> l # ordonne sur le premier élément, puis le second si il y a égalité
[(1, 2), (2, 0), (2, 1), (2, 2), (2, 3), (7, 8)]
>>> persos = [PersonnageDeCityHunter('Ryo Saeba', 99999), PersonnageDeCityHunter('Mamouth', 1), PersonnageDeCityHunter('Kaori', 0)]
>>> persos.sort()
>>> for perso in persos:
...     print perso.nom
...     
Kaori
Mamouth
Ryo Saeba

On peut demander à inverser l’odre de tri en passant l’argument reverse:

>>> l = [1, 7, 3, 8]
>>> l.sort(reverse=True)
>>> l
[8, 7, 3, 1]

Il n’y a pas que les listes dans la vie

Tout itérable est ordonnable, et la fonction sorted() permet justement d’ordonner n’importe quel itérable de taille finie.

>>> sorted([1, 7, 3, 8]) # une liste
[1, 3, 7, 8]
>>> sorted(set([1, 7, 3, 8])) # un set
[1, 3, 7, 8]
>>> sorted((1, 7, 3, 8)) # un tuple
[1, 3, 7, 8]
>>> sorted('Un clavier azerty en vaut deux') # une chaîne
[' ', ' ', ' ', ' ', ' ', 'U', 'a', 'a', 'a', 'c', 'd', 'e', 'e', 'e', 'e', 'i', 'l', 'n', 'n', 'r', 'r', 't', 't, 'u', 'u', 'v', 'v', 'x', 'y', 'z']
>>> sorted(open('/etc/fstab'))[:3] # un fichier
['#\n', '#\n', '# / was on /dev/sda5 during installation\n']
>>> import random # plus ou moins n'importe quoi
>>> sorted([random.randint(0, 100) for x in range(random.randint(0, 20)) if x * 2 not in (42, 69)])
[2, 4, 21, 35, 37, 41, 48, 48, 54, 58, 58, 59, 62, 76, 82, 94]

sort() et sorted() acceptent toutes les deux les mêmes arguments. Ce que vous apprenez pour l’un vaut pour l’autre. La seul différence est que sort() retourne None et modifie sur place, tandis que sorted() retourne une nouvelle liste. sorted() est un peu moins performant.

>>> sorted((1, 7, 3, 8), reverse=True)
[8, 7, 3, 1]

Tri personnalisé avec key

Parfois on a besoin de trier sur quelque chose de plus complexe qu’une lettre ou un chiffre. Par exemple, vous avez des scores dans un dictionnaires. Un dictionnaire n’est pas ordonné. Si vous imprimez un classement, il faut en faire une liste de tuples :

>>> scores = {'Robert': 2, 'Roger': 1, 'Gertrude': 4, 'Cunegonde': 3}
>>> scores.items()
[('Gertrude', 4), ('Cunegonde', 3), ('Robert', 2), ('Roger', 1)]

Et il faut maintenant ordonner ça sur le deuxième élément de chaque tuple : le score. Seulement si on appelle sorted(), il va trier sur l’ordre naturel, donc il va commencer par le premier élément, ça ne marchera pas :

>>> sorted(scores.items())
[('Cunegonde', 3), ('Gertrude', 4), ('Robert', 2), ('Roger', 1)]

C’est là qu’intervient le paramètre key. key est très particulier, c’est un paramètre qui attend qu’on lui passe un callback, donc key attend qu’on lui passe une fonction.

Pas le résultat d’une fonction. La fonction elle même.

Cette fonction doit attendre en paramètre un élément, et en extraire la partie sur laquelle on veut trier.

Par exemple dans le cas des scores, on doit passer à key une fonction qui attend un tuple en paramètre, et retourne le score :

>>> def get_score(nom_et_score):
...     return nom_et_score[1] # retourne le 2nd element du tuple
... 
>>> sorted(scores.items(), key=get_score) # on passe la fonction a key
[('Roger', 1), ('Robert', 2), ('Cunegonde', 3), ('Gertrude', 4)]

Et voilà, c’est trié dans le bon ordre car sorted() va utiliser get_score() pour récupérer la valeur de chaque élément un par un, sur laquelle on va trier la liste (selon l’ordre naturel de cette valeur).

On peut utiliser key pour faire des choses beaucoup plus complexes, comme comparer sur des valeurs qui n’existent pas, mais que l’on calcule :

>>> def carre(val): # on va ordonner par valeur de carré
...     return val * val 
... 
>>> sorted([-1, -2, 0, 3], key=carre)
[0, -1, -2, 3]

Ici, un carré est toujours positif, du coup on retrouve -1 et -2 devant 0, car leurs carrés 1 et 4 sont plus grands que le carré 0 de 0.

Ne vous embrouillez pas : la valeur retournée par carre() n’est pas MISE dans la liste, elle est juste utilisée pour choisir l’ordre d’un élément dans la liste.

On peut même faire des comparaisons très avancées, comme par exemple comparer sur plusieurs attributs d’un objet. Imaginez, je veux ordonner des objets voitures selon leur coût d’entretien d’abord, et ensuite par coût d’achat.

class Voiture(object):
 
    def __init__(self, cout_entretien, cout_achat):
        self.cout_entretien = cout_entretien
        self.cout_achat = cout_achat
 
    def __repr__(self):
        return "<Voiture E-{} / A-{}>".format(self.cout_entretien, self.cout_achat)
 
>>> voitures = [Voiture(10000, 10000), Voiture(50000, 10000), Voiture(10000, 60000)]
>>> voitures
[<Voiture E-10000 / A-10000>, <Voiture E-50000 / A-10000>, <Voiture E-10000 / A-60000>]
>>> def get_entretien_achat(voiture): 
...         return (voiture.cout_entretien, voiture.cout_achat)
... 
>>> sorted(voitures, key=get_entretien_achat)
[<Voiture E-10000 / A-10000>, <Voiture E-10000 / A-60000>, <Voiture E-50000 / A-10000>]

get_entretien_achat() retourne un tuple de deux valeurs. Python va donc ordonner sur ce tuple, dans l’odre naturel du tuple en prenant la première valeur comme point de comparaison (coût d’entretien), puis la seconde (coût d’achat) si les valeurs sont égales.

On peut donc comparer des objets arbitraires sur des critères arbitraires.

La plupart du temps, vous voudrez juste comparer sur un élément ou un attribut, donc dans ce cas le module operator (ou une lambda) est votre ami :

>>> from operator import attrgetter, itemgetter
>>> sorted(voitures, key=attrgetter('cout_achat')) # ordonner par cout d'achat
[<Voiture E-10000 / A-10000>, <Voiture E-50000 / A-10000>, <Voiture E-10000 / A-60000>]
>>> sorted(scores.items(), key=itemgetter(1)) # ordonner par score
[('Roger', 1), ('Robert', 2), ('Cunegonde', 3), ('Gertrude', 4)]
>>> sorted(scores.items(), key=itemgetter(1), reverse=True) 
[('Gertrude', 4), ('Cunegonde', 3), ('Robert', 2), ('Roger', 1)]

Pour finir, je vous invite à lire l’article sur le module heapq qui lui permet de faire des tris sur des flux de données sans taille définie.