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.
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.
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.
O(1)
par index).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 )
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é.<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!)==
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 .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?
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:
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
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".
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}