web-dev-qa-db-fra.com

Comment Python dict peut-il avoir plusieurs clés avec le même hachage?

J'essaie de comprendre python fonction de hachage sous le capot. J'ai créé une classe personnalisée où toutes les instances renvoient la même valeur de hachage.

class C(object):
    def __hash__(self):
        return 42

Je viens de supposer qu'une seule instance de la classe ci-dessus peut être dans un ensemble à tout moment, mais en fait, un ensemble peut avoir plusieurs éléments avec le même hachage.

c, d = C(), C()
x = {c: 'c', d: 'd'}
print x
# {<__main__.C object at 0x83e98cc>:'c', <__main__.C object at 0x83e98ec>:'d'}
# note that the dict has 2 elements

J'ai expérimenté un peu plus et j'ai découvert que si je remplaçais le __eq__ méthode telle que toutes les instances de la classe se comparent égales, alors l'ensemble n'autorise qu'une seule instance.

class D(C):
    def __eq__(self, other):
        return hash(self) == hash(other)

p, q = D(), D()
y = {p:'p', q:'q'}
print y
# {<__main__.D object at 0x8817acc>]: 'q'}
# note that the dict has only 1 element

Je suis donc curieux de savoir comment un dict peut avoir plusieurs éléments avec le même hachage. Merci!

Remarque: Modification de la question pour donner un exemple de dict (au lieu de set) car toute la discussion dans les réponses concerne les dict. Mais la même chose s'applique aux ensembles; les ensembles peuvent également avoir plusieurs éléments avec la même valeur de hachage.

78
Praveen Gollakota

Pour une description détaillée du fonctionnement du hachage de Python, voir ma réponse à Pourquoi le retour anticipé est-il plus lent qu'autre?

Fondamentalement, il utilise le hachage pour choisir un emplacement dans la table. S'il y a une valeur dans l'emplacement et que le hachage correspond, il compare les éléments pour voir s'ils sont égaux.

Si le hachage ne correspond pas ou si les éléments ne sont pas égaux, il essaie un autre emplacement. Il y a une formule pour choisir cela (que je décris dans la réponse référencée), et il extrait progressivement les parties inutilisées de la valeur de hachage; mais une fois qu'il les aura tous utilisés, il finira par se frayer un chemin dans tous les emplacements de la table de hachage. Cela garantit finalement que nous trouvons un article correspondant ou un emplacement vide. Lorsque la recherche trouve un emplacement vide, elle insère la valeur ou abandonne (selon que nous ajoutons ou obtenons une valeur).

La chose importante à noter est qu'il n'y a pas de listes ou de compartiments: il y a juste une table de hachage avec un nombre particulier d'emplacements, et chaque hachage est utilisé pour générer une séquence d'emplacements candidats.

42
Duncan

Voici tout sur Python dict que j'ai pu assembler (probablement plus que quiconque voudrait savoir; mais la réponse est complète). Un cri à Duncan = pour avoir souligné que Python dict utilise des emplacements et me conduit dans ce trou de lapin.

  • Les dictionnaires Python sont implémentés comme tables de hachage .
  • Les tables de hachage doivent permettre les collisions de hachage c'est-à-dire que même si deux clés ont la même valeur de hachage, l'implémentation de la table doit avoir une stratégie pour insérer et récupérer la clé et paires de valeurs sans ambiguïté.
  • Python dict utilise l'adressage ouvert pour résoudre les collisions de hachage (expliqué ci-dessous) (voir dictobject.c: 296-297 ).
  • La table de hachage Python est juste un bloc de mémoire contingent (un peu comme un tableau, donc vous pouvez faire une recherche O(1) par index).
  • Chaque emplacement du tableau peut stocker une et une seule entrée. Ceci est important
  • Chaque entrée dans le tableau est en fait une combinaison des trois valeurs -. Ceci est implémenté comme une structure C (voir dictobject.h: 51-56 )
  • La figure ci-dessous est une représentation logique d'une table de hachage python. Dans la figure ci-dessous, 0, 1, ..., i, ... à gauche sont des indices de la emplacements dans la table de hachage (ils sont juste à des fins d'illustration et ne sont pas stockés avec la table évidemment!).

    # Logical model of Python Hash table
    -+-----------------+
    0| <hash|key|value>|
    -+-----------------+
    1|      ...        |
    -+-----------------+
    .|      ...        |
    -+-----------------+
    i|      ...        |
    -+-----------------+
    .|      ...        |
    -+-----------------+
    n|      ...        |
    -+-----------------+
    
  • Lorsqu'un nouveau dict est initialisé, il commence par 8 slots. (voir dictobject.h: 49 )

  • Lors de l'ajout d'entrées à la table, nous commençons par un emplacement, i qui est basé sur le hachage de la clé. CPython utilise l'initiale i = hash(key) & mask. Où mask = PyDictMINSIZE - 1, Mais ce n'est pas vraiment important). Notez simplement que l'emplacement initial, i, qui est vérifié dépend du - hash de la clé.
  • Si cet emplacement est vide, l'entrée est ajoutée à l'emplacement (par entrée, je veux dire, <hash|key|value>). Mais que faire si cet emplacement est occupé!? Très probablement parce qu'une autre entrée a le même hachage (collision de hachage!)
  • Si l'emplacement est occupé, CPython (et même PyPy) compare le le hachage ET la clé (par comparer je veux dire == Comparaison pas la comparaison is) de l'entrée dans l'emplacement par rapport à la clé de l'entrée actuelle à insérer ( dictobject.c: 337 , 44-345 ) . Si les deux correspondent, alors il pense que l'entrée existe déjà, abandonne et passe à l'entrée suivante à insérer. Si le hachage ou la clé ne correspondent pas, il démarre le sondage .
  • Le sondage signifie simplement qu'il recherche les emplacements par emplacement pour trouver un emplacement vide. Techniquement, nous pourrions simplement aller un par un, i + 1, i + 2, ... et utiliser le premier disponible (c'est-à-dire le palpage linéaire). Mais pour des raisons expliquées magnifiquement dans les commentaires (voir dictobject.c: 33-126 ), CPython utilise un sondage aléatoire . Dans le sondage aléatoire, le créneau suivant est sélectionné dans un ordre pseudo aléatoire. L'entrée est ajoutée au premier emplacement vide. Pour cette discussion, l'algorithme réel utilisé pour choisir l'emplacement suivant n'est pas vraiment important (voir dictobject.c: 33-126 pour l'algorithme de sondage). Ce qui est important, c'est que les logements soient sondés jusqu'à ce que le premier logement vide soit trouvé.
  • La même chose se produit pour les recherches, commence juste avec l'emplacement initial i (où i dépend du hachage de la clé). Si le hachage et la clé ne correspondent pas à l'entrée de l'emplacement, il commence à sonder jusqu'à ce qu'il trouve un emplacement avec une correspondance. Si tous les emplacements sont épuisés, il signale un échec.
  • BTW, le dict sera redimensionné s'il est plein aux deux tiers. Cela évite de ralentir les recherches. (voir dictobject.h: 64-65 )

Voilà! L'implémentation Python de dict vérifie à la fois l'égalité de hachage de deux clés et l'égalité normale (==) Des clés lors de l'insertion d'éléments. Donc en résumé, s'il y a deux clés , a et b et hash(a)==hash(b), mais a!=b, alors les deux peuvent exister harmonieusement dans un Python dict. Mais si hash(a)==hash(b) et a==b, alors ils ne peuvent pas tous les deux être dans le même dict.

Parce que nous devons sonder après chaque collision de hachage, un effet secondaire d'un trop grand nombre de collisions de hachage est que les recherches et les insertions deviendront très lentes (comme Duncan le souligne dans les commentaires ).

Je suppose que la réponse courte à ma question est: "Parce que c'est comme ça que c'est implémenté dans le code source;)"

Bien que cela soit bon à savoir (pour les points geek?), Je ne sais pas comment cela peut être utilisé dans la vraie vie. Parce que, sauf si vous essayez de casser explicitement quelque chose, pourquoi deux objets qui ne sont pas égaux auraient-ils le même hachage?

94
Praveen Gollakota

Edit : la réponse ci-dessous est l'un des moyens possibles de gérer les collisions de hachage, mais ( pas comment Python le fait. Le wiki de Python référencé ci-dessous est également incorrect. La meilleure source donnée par @Duncan ci-dessous est l'implémentation elle-même: http: // svn .python.org/projects/python/trunk/Objects/dictobject.c Je m'excuse pour la confusion.


Il stocke une liste (ou un compartiment) d'éléments au niveau du hachage, puis parcourt cette liste jusqu'à ce qu'il trouve la clé réelle dans cette liste. Une image en dit plus que mille mots:

Hash table

Ici vous voyez John Smith et Sandra Dee les deux hachages à 152. Seau 152 contient les deux. En recherchant Sandra Dee il trouve d'abord la liste dans le compartiment 152, puis parcourt cette liste jusqu'à Sandra Dee est trouvé et renvoie 521-6955.

Ce qui suit est faux, c'est seulement ici pour le contexte: Sur wiki de Python vous pouvez trouver (pseudo?) Le code comment Python effectue la recherche.

Il existe en fait plusieurs solutions possibles à ce problème, consultez l'article de wikipedia pour une belle vue d'ensemble: http://en.wikipedia.org/wiki/Hash_table#Collision_resolution

19
Rob Wouters

Les tables de hachage doivent en général permettre les collisions de hachage! Vous n'aurez pas de chance et deux choses finiront par hacher la même chose. En dessous, il y a un ensemble d'objets dans une liste d'éléments qui a la même clé de hachage. Habituellement, il n'y a qu'une seule chose dans cette liste, mais dans ce cas, elle continuera de les empiler dans la même. La seule façon dont il sait qu'ils sont différents est par l'opérateur égal.

Lorsque cela se produit, vos performances se dégradent avec le temps, c'est pourquoi vous souhaitez que votre fonction de hachage soit aussi "aléatoire que possible".

4
Donald Miner

Dans le fil, je n'ai pas vu exactement ce que python fait avec les instances d'une classe définie par l'utilisateur lorsque nous le mettons dans un dictionnaire sous forme de clés. Lisons de la documentation: il déclare que seuls les objets hachables peuvent être utilisées comme clés. Hashable sont toutes les classes intégrées immuables et toutes les classes définies par l'utilisateur.

Les classes définies par l'utilisateur ont par défaut les méthodes __cmp __ () et __hash __ (); avec eux, tous les objets sont différents (sauf avec eux-mêmes) et x .__ hash __ () renvoie un résultat dérivé de id (x).

Donc, si vous avez constamment un __hash__ dans votre classe, mais ne fournissez aucune méthode __cmp__ ou __eq__, alors toutes vos instances sont inégales pour le dictionnaire. En revanche, si vous fournissez une méthode __cmp__ ou __eq__, mais ne fournissez pas __hash__, vos instances sont toujours inégales en termes de dictionnaire.

class A(object):
    def __hash__(self):
        return 42


class B(object):
    def __eq__(self, other):
        return True


class C(A, B):
    pass


dict_a = {A(): 1, A(): 2, A(): 3}
dict_b = {B(): 1, B(): 2, B(): 3}
dict_c = {C(): 1, C(): 2, C(): 3}

print(dict_a)
print(dict_b)
print(dict_c)

Sortie

{<__main__.A object at 0x7f9672f04850>: 1, <__main__.A object at 0x7f9672f04910>: 3, <__main__.A object at 0x7f9672f048d0>: 2}
{<__main__.B object at 0x7f9672f04990>: 2, <__main__.B object at 0x7f9672f04950>: 1, <__main__.B object at 0x7f9672f049d0>: 3}
{<__main__.C object at 0x7f9672f04a10>: 3}
2
checkraise