Python: comment le functools cmp à la clé fonctionne?

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

les anciennes versions de Python ont utilisé une approche différente en utilisant le cmp paramètre à 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 au second, nul s'il est égal et d'un nombre positif si le premier est grand. À 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 qu' cmp la fonction était beaucoup plus facile à écrire qu'un key. Je n'ai pas envie d'utiliser une fonctionnalité obsolète alors j'ai lu la documentation et j'ai trouvé qu'il existe une fonction nommée cmp_to_key dans le functools module qui, comme son nom l'indique, reçoit un cmp la fonction et renvoie un key... ou qu'est ce que je pensais jusqu'à ce que je lise le code source (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

Malgré le fait que cmp_to_key fonctionne comme prévu, je reçois surpris par le fait que cette fonction ne retourne pas une fonction mais un K classe à la place. Pourquoi? Comment ça fonctionne? Je pense que le sorted fonction vérifie en interne si cmp est une fonction ou une classe K ou quelque chose de similaire, mais je ne suis pas assurer.

P.S.: malgré sa maîtrise de la langue, j'ai trouvé que la classe K est très utile. Cochez 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

16
demandé sur matiascelasco 2015-09-24 06:16:47

3 réponses

Non, sorted (ou list.sort) à l'interne n'a pas besoin de vérifier si l'objet reçu est une fonction ou une classe . Tous il se soucie est que l'objet qu'il a reçu dans key argument devrait être exigible et doit retourner une valeur qui peut être comparé à d'autres valeurs, lorsqu'il est appelé.

Classes sont également remboursables , lorsque vous appelez une classe , vous recevez l'instance de cette classe.

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

  1. key appelable est appelée pour chaque élément et il reçoit en retour 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 exigible avec l'autre élément).

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

maintenant sur votre code équivalent, quand vous créez une instance de cette classe, elle peut être comparée à d'autres instances de la même classe en utilisant votre mycmp fonction. Et trier lors du tri des valeurs compare ces objets (in-effect) appelant votre mycmp() fonction pour déterminer si la valeur est inférieure ou supérieure à l'autre de l'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]
12
répondu Anand S Kumar 2015-09-24 03:32:29

je viens de réaliser que, bien que n'étant pas une fonction, la classe K est un callable, parce que c'est classe! et les classes sont des callables qui, lorsqu'elles sont appelées, créent une nouvelle instance, l'initialisent en appelant le__init__ et renvoie ensuite cette instance.

de cette façon il se comporte comme un key fonction parce que K reçoit l'objet lorsqu'il est appelé, et enveloppe cet objet dans une instance K, qui peut être comparée à D'autres instances K.

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

1
répondu matiascelasco 2015-09-24 03:31:05

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

si j'essaie de créer une sorte 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 sort est seulement stable à chaque appel, le documentation est trompeuse de l'omi:

la méthode sort () est garantie d'être stable. Un tri est stable s'il garantit de ne pas changer l'ordre relatif des éléments qui comparent equal - c'est utile pour le tri en passes multiples (par exemple, Tri par département, puis par grade salarial).

l'approche de l'ancien 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 important la différence, c'est qu'on ne peut pas faire plusieurs passes juste en dehors de la boîte. Les valeurs/résultats de la fonction clé doivent être relatifs sortables dans l'ordre, pas les éléments à trier. C'est pourquoi le masque cmp_to_key est le suivant: créez les objets comparables dont vous avez besoin pour les commander.

J'espère que ça aidera. et merci pour l'aperçu dans le cmp_to_key code, m'a beaucoup aidé :)

1
répondu seishin 2015-12-20 08:09:15