web-dev-qa-db-fra.com

Python: comment fonctionne la fonction functools cmp_to_key?

En Python, les deux list.sort méthode et sorted fonction intégrée acceptent un paramètre facultatif nommé key, qui est une fonction qui, étant donné un élément de la liste, renvoie sa clé de tri.

Anciennes Python utilisaient une approche différente en utilisant le paramètre cmp à la place, qui est une fonction qui, étant donné deux éléments de la liste, renvoie un nombre négatif si le premier est inférieur à la deuxièmement, zéro s'il y a des égaux et un nombre positif si le premier est supérieur. À un moment donné, ce paramètre a été déprécié et n'a pas été inclus dans Python 3.

L'autre jour, j'ai voulu trier une liste d'éléments de manière à ce qu'une fonction cmp soit beaucoup plus facile à écrire qu'une fonction key. Je ne voulais pas utiliser une fonctionnalité obsolète alors j'ai lu la documentation et j'ai trouvé qu'il y avait une fonction nommée cmp_to_key dans le module functools qui, comme son nom l'indique, reçoit une fonction cmp et retourne une key une ... ou c'est ce que je pensais jusqu'à ce que je lise la source code (ou au moins une version équivalente) de cette fonction de haut niveau incluse dans le docs

def cmp_to_key(mycmp):
    'Convert a cmp= function into a key= function'
    class K(object):
        def __init__(self, obj, *args):
            self.obj = obj
        def __lt__(self, other):
            return mycmp(self.obj, other.obj) < 0
        def __gt__(self, other):
            return mycmp(self.obj, other.obj) > 0
        def __eq__(self, other):
            return mycmp(self.obj, other.obj) == 0
        def __le__(self, other):
            return mycmp(self.obj, other.obj) <= 0
        def __ge__(self, other):
            return mycmp(self.obj, other.obj) >= 0
        def __ne__(self, other):
            return mycmp(self.obj, other.obj) != 0
    return K

En dépit du fait que cmp_to_key fonctionne comme prévu, je suis surpris par le fait que cette fonction ne retourne pas une fonction mais une classe K à la place. Pourquoi? Comment ça marche? Je suppose que la fonction sorted vérifie en interne si cmp est une fonction ou une classe K ou quelque chose de similaire, mais je ne suis pas sûr.

P.S.: Malgré cette bizarrerie, j'ai trouvé que la classe K est très utile. Vérifiez ce code:

from functools import cmp_to_key

def my_cmp(a, b):
    # some sorting comparison which is hard to express using a key function

class MyClass(cmp_to_key(my_cmp)):
    ...

De cette façon, toute liste d'instances de MyClass peut être, par défaut, triée selon les critères définis dans my_cmp

29
matiascelasco

Non, la fonction sorted (ou list.sort) En interne n'a pas besoin de vérifier si l'objet qu'elle a reçu est une fonction ou une classe. Tout ce qui lui importe, c'est que l'objet qu'il a reçu dans l'argument key soit appelable et retourne une valeur qui peut être comparée à d'autres valeurs lors de son appel.

Les classes sont également appelables, lorsque vous appelez une classe, vous recevez l'instance de cette classe.

Pour répondre à votre question, nous devons d'abord comprendre (au moins au niveau de base) comment fonctionne l'argument key -

  1. L'appelable key est appelé pour chaque élément et il reçoit l'objet avec lequel il doit trier.

  2. Après avoir reçu le nouvel objet, il compare ceci à d'autres objets (encore reçu en appelant le key appelable avec l'élément othe).

Maintenant, la chose importante à noter ici est que le nouveau object reçu est comparé à d'autres mêmes objets.

Maintenant sur votre code équivalent, lorsque vous créez une instance de cette classe, elle peut être comparée à d'autres instances de la même classe en utilisant votre fonction mycmp. Et le tri lors du tri des valeurs compare ces objets (en effet) en appelant votre fonction mycmp() pour déterminer si la valeur est inférieure ou supérieure à l'autre objet.

Exemple avec des instructions d'impression -

>>> def cmp_to_key(mycmp):
...     'Convert a cmp= function into a key= function'
...     class K(object):
...         def __init__(self, obj, *args):
...             print('obj created with ',obj)
...             self.obj = obj
...         def __lt__(self, other):
...             print('comparing less than ',self.obj)
...             return mycmp(self.obj, other.obj) < 0
...         def __gt__(self, other):
...             print('comparing greter than ',self.obj)
...             return mycmp(self.obj, other.obj) > 0
...         def __eq__(self, other):
...             print('comparing equal to ',self.obj)
...             return mycmp(self.obj, other.obj) == 0
...         def __le__(self, other):
...             print('comparing less than equal ',self.obj)
...             return mycmp(self.obj, other.obj) <= 0
...         def __ge__(self, other):
...             print('comparing greater than equal',self.obj)
...             return mycmp(self.obj, other.obj) >= 0
...         def __ne__(self, other):
...             print('comparing not equal ',self.obj)
...             return mycmp(self.obj, other.obj) != 0
...     return K
...
>>> def mycmp(a, b):
...     print("In Mycmp for", a, ' ', b)
...     if a < b:
...         return -1
...     Elif a > b:
...         return 1
...     return 0
...
>>> print(sorted([3,4,2,5],key=cmp_to_key(mycmp)))
obj created with  3
obj created with  4
obj created with  2
obj created with  5
comparing less than  4
In Mycmp for 4   3
comparing less than  2
In Mycmp for 2   4
comparing less than  2
In Mycmp for 2   4
comparing less than  2
In Mycmp for 2   3
comparing less than  5
In Mycmp for 5   3
comparing less than  5
In Mycmp for 5   4
[2, 3, 4, 5]
22
Anand S Kumar

Je n'ai pas regardé la source, mais je pense que le résultat de la fonction clé peut également être n'importe quoi, et donc aussi un objet comparable. Et cmp_to_key masque simplement la création de ces K objets, qui sont ensuite comparés les uns aux autres pendant que sort fait son travail.

Si j'essaie de créer un tri sur les départements et inverser les numéros de salle comme ceci:

departments_and_rooms = [('a', 1), ('a', 3),('b', 2)]
departments_and_rooms.sort(key=lambda vs: vs[0])
departments_and_rooms.sort(key=lambda vs: vs[1], reverse=True)
departments_and_rooms # is now [('a', 3), ('b', 2), ('a', 1)]

Ce n'est pas ce que je veux, et je pense que le tri n'est stable qu'à chaque appel, le documentation est trompeur imo:

La méthode sort () est garantie d'être stable. Un tri est stable s'il garantit de ne pas modifier l'ordre relatif des éléments qui se comparent égaux - cela est utile pour trier en plusieurs passes (par exemple, trier par département, puis par niveau de salaire).

L'ancienne approche de style fonctionne parce que chaque résultat appelant la classe K renvoie une instance K et se compare aux résultats de mycmp:

def mycmp(a, b):                             
    return cmp((a[0], -a[1]), (b[0], -b[1]))

departments_and_rooms = [('a', 1), ('a', 3),('b', 2)]
departments_and_rooms.sort(key=cmp_to_key(mycmp))
departments_and_rooms # is now [('a', 3), ('a', 1), ('b', 2)]

C'est une différence importante, que l'on ne peut pas faire plusieurs passes juste à la sortie de la boîte. Les valeurs/résultats de la fonction clé doivent être triables dans l'ordre, et non les éléments à trier. C'est donc le masque cmp_to_key: créez les objets comparables dont vous avez besoin pour les commander.

J'espère que cela pourra aider. et merci pour la perspicacité dans le code cmp_to_key, m'a beaucoup aidé aussi :)

1
seishin

Je viens de réaliser que, bien qu'elle ne soit pas une fonction, la classe K est appelable, car c'est une classe! et les classes sont appelables qui, lorsqu'elles sont appelées, créent une nouvelle instance, l'initialise en appelant le __init__, puis retourne cette instance.

De cette façon, il se comporte comme une fonction key car K reçoit l'objet lorsqu'il est appelé et encapsule cet objet dans une instance K, qui peut être comparée à d'autres instances K.

Corrige moi si je me trompe. J'ai l'impression d'entrer dans le territoire des méta-classes que je ne connais pas.

1
matiascelasco