web-dev-qa-db-fra.com

Comment python calcule le hachage d'un tuple

En python, si j'ai un tuple avec de nombreux éléments, son hachage est-il calculé à partir du _ ids de ses éléments ou du contenu de ses éléments?

Dans cet exemple,

a = (1, [1,2])
hash(a)

Il se trompe en disant que la liste n'est pas partageable. Donc je suppose que ce n'est pas calculé par id, ou probablement il y a une vérification pour savoir si l'élément est mutable.

Maintenant, voyez cet exemple

class A: pass
a0 = A()
ta = (1, a0)
hash(ta)  # -1122968024
a0.x = 20
hash(ta)  # -1122968024

Ici, il s'avère que le hachage de ta ne change pas avec la modification de son élément, c'est-à-dire a0. Alors peut-être que l'ID de a0 Est utilisé pour le calcul du hachage? a0 Est-il en quelque sorte considéré comme immuable? Comment python sait-il si un type est mutable?

Considérons maintenant ce cas

b = (1, 2)
id(b)  # 3980742764
c = (1, 2)
id(c)  # 3980732588
tb = (1, b)
tc = (1, c) 
hash(tb)  # -1383040070
hash(tc)  # -1383040070

Il semble que le contenu de b et c soit utilisé pour le calcul du hachage.

Comment dois-je comprendre ces exemples?

29
nos

Ni. Il est calculé sur la base des hachages de ces éléments, et non de leur "contenu" (valeurs/attributs).

Jetez un œil à ce paragraphe dans glossaire de documentation de python .

Que quelque chose est hachable ou non, et comment il soit haché, dépend de l'implémentation de sa méthode .__hash__(). Python lui-même n'a aucune idée de la mutabilité d'un objet.

Dans votre premier exemple, Tuple arrive à se hacher lui-même sur la base de ses éléments, tandis qu'un list n'a pas de hachage du tout - la méthode .__hash__() n'est pas mis en œuvre pour cela (et pour une bonne raison). C'est pourquoi un Tuple avec un objet list à l'intérieur n'est pas hachable.

Maintenant, ayant cela à l'esprit, jetons un coup d'œil à documentation du modèle de données python , et ce qu'il a à dire sur le sujet:

Les classes définies par l'utilisateur ont par défaut les méthodes __eq__() et __hash__(); avec eux, tous les objets sont différents (sauf avec eux-mêmes) et x.__hash__() renvoie une valeur appropriée telle que x == y implique à la fois que x is y et hash(x) == hash(y).

C'est pourquoi vous n'avez pas à définir .__hash__() pour vos classes - python le fait pour vous dans ce cas. L'implémentation par défaut ne prend pas en compte les champs d'instance cependant C'est pourquoi vous pouvez changer les valeurs à l'intérieur de votre objet sans changer son hachage.

À cet égard, vous avez raison - l'implémentation par défaut ( CPython) de la fonction de hachage pour les classes personnalisées repose sur id() d'un objet , et non sur les valeurs qu'il contient. Il s'agit d'un détail d'implémentation, et il diffère cependant entre les versions Python. Dans les versions plus récentes de Python la relation entre hash() et id() implique une randomisation.


Mais comment se hache-t-il réellement?

Bien que les détails soient assez compliqués et impliquent probablement des mathématiques avancées, l'implémentation de la fonction de hachage pour les objets Tuple est écrite en C, et peut être vue ici (voir static Py_hash_t tuplehash(PyTupleObject *v).

Le calcul implique XORing une constante avec les hachages de chacun des éléments du Tuple. La ligne responsable du hachage des éléments est celle-ci:

y = PyObject_Hash(*p++);

Donc, pour répondre à votre question d'origine: il fait un tas de XOR hokus-pocus avec les hachages de chacun de ses éléments L'utilisation ou non du contenu de ces éléments dépend de leurs fonctions de hachage spécifiques.

25

Le contrat de base du hachage est que les objets égaux ont des hachages égaux. En particulier, le hachage ne se soucie pas directement de la mutabilité ou de la mutation; il ne se soucie que de mutation qui affecte les comparaisons d'égalité.


Votre premier tuple n'est pas partageable, car la mutation de la liste imbriquée changerait son comportement dans les comparaisons d'égalité.

Mutation a0 dans votre deuxième exemple n'affecte pas le hachage du Tuple car il n'affecte pas les comparaisons d'égalité. a0 n'est toujours égal qu'à lui-même et son hachage est inchangé.

tb et tc dans votre troisième exemple ont des hachages égaux car ce sont des tuples égaux, que leurs éléments soient ou non les mêmes objets.


Cela signifie que les tuples ne peuvent pas (directement) utiliser id pour les hachages. S'ils le faisaient, des tuples égaux avec des éléments distincts mais égaux pourraient hacher différemment, violant le contrat de hachage. Sans types d'élément à casse spéciale, les seuls éléments que les tuples peuvent utiliser pour calculer leurs propres hachages sont les hachages de leurs éléments, donc les tuples basent leurs hachages sur les hachages de leurs éléments.

8
user2357112

La réponse à la question "Le hachage du tuple est-il calculé en fonction de l'identité ou de la valeur?" est ni.

La bonne réponse est que le hachage du tuple est calculé à partir des hachages des éléments. Comment ces hachages sont calculés est (plus ou moins) sans importance.

Un moyen simple de le prouver est de voir ce qui se passe lorsque vous mettez une liste dans un tuple:

>>> hash( (1, 2) )
3713081631934410656
>>> hash( (1, []) )
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

Parce que les listes ne sont pas lavables, un Tuple contenant une liste n'est pas non plus lavable.


Examinons de plus près cet exemple que vous avez apporté:

class A: pass
a0 = A()
ta = (1, a0)
hash(ta)  # -1122968024
a0.x = 20
hash(ta)  # -1122968024

Pourquoi le paramètre a0.x = 20 N'affecte-t-il pas le hachage du tuple? Eh bien, si nous modifions ce code pour afficher le hachage de a0, Vous verrez que le paramètre a0.x = 20 N'a aucun effet sur la valeur de hachage de a0:

a0 = A()
print(hash(a0))  # -9223363274645980307
a0.x = 20
print(hash(a0))  # -9223363274645980307

La raison en est que python implémente une fonction de hachage par défaut pour vous. De les docs :

Les classes définies par l'utilisateur ont par défaut les méthodes __eq__() et __hash__(); avec eux, tous les objets sont différents (sauf avec eux-mêmes) et x.__hash__() renvoie une valeur appropriée telle que x == y implique à la fois que x is y et hash(x) == hash(y).

La fonction de hachage par défaut ignore les attributs de l'objet et calcule le hachage en fonction de l'ID de l'objet. Peu importe les modifications que vous apportez à a0, Son hachage restera toujours le même. (Bien qu'il soit possible de définir une fonction de hachage personnalisée pour les instances de votre classe A en implémentant une méthode personnalisée __hash__ .)


Addendum: La raison pour laquelle les listes ne sont pas lavables est qu'elles sont modifiables. De les docs :

Si une classe définit des objets mutables et implémente une méthode __eq__(), elle ne doit pas implémenter __hash__(), car l'implémentation de collections lavables nécessite que la valeur de hachage d'une clé soit immuable (si la valeur de hachage de l'objet changements, il sera dans le mauvais compartiment de hachage).

Les listes entrent dans cette catégorie.

3
Aran-Fey

le hachage d'un Tuple est basé sur le contents, pas sur les _id_s des tuples. Et les hachages sont calculés de manière récursive: si un élément n'est pas lavable (comme un élément list), alors le tuple lui-même n'est pas lavable.

Il est parfaitement normal que si a et b sont des tuples et a == b, Alors hash(a) == hash(b) (si les hachages peuvent être calculés bien sûr), même si a is not b.

(au contraire hash(a) == hash(b) ne signifie pas que a == b)

Les informations véhiculées par is ne sont souvent pas très utiles, à cause de l'internement d'objet python par exemple.

2