web-dev-qa-db-fra.com

Pourquoi une clé de dictionnaire à virgule flottante peut-elle remplacer une clé entière avec la même valeur?

Je travaille par http://www.mypythonquiz.com , et question # 45 demande la sortie du code suivant:

confusion = {}
confusion[1] = 1
confusion['1'] = 2
confusion[1.0] = 4

sum = 0
for k in confusion:
    sum += confusion[k]

print sum

La sortie est 6, puisque la clé 1.0 remplace 1. Cela me semble un peu dangereux, est-ce jamais une fonctionnalité de langue utile?

52
sjdenny

Vous devez considérer que dict vise à stocker des données en fonction de la valeur numérique logique, et non de la façon dont vous les avez représentées.

La différence entre ints et floats n'est en effet qu'un détail d'implémentation et non conceptuel. Idéalement, le seul type de nombre devrait être un nombre de précision arbitraire avec une précision illimitée, voire une sous-unité ... ceci est cependant difficile à mettre en œuvre sans avoir de problèmes ... mais peut-être que ce sera le seul futur type numérique pour Python.

Ainsi, tout en ayant différents types pour des raisons techniques Python essaie de masquer ces détails d'implémentation et la conversion int-> float est automatique.

Il serait beaucoup plus surprenant que dans un programme Python if x == 1: ... n'allait pas être prise lorsque x est un float avec la valeur 1.

Notez également qu'avec Python 3 la valeur de 1/2 est 0.5 (la division de deux entiers) et que les types long et la chaîne non unicode ont été supprimés avec la même tentative de masquer les détails de l'implémentation.

17
6502

Tout d'abord: le comportement est documenté explicitement dans la documentation de la fonction hash :

hash(object)

Renvoie la valeur de hachage de l'objet (s'il en a une). Les valeurs de hachage sont des entiers. Ils sont utilisés pour comparer rapidement les clés de dictionnaire lors d'une recherche de dictionnaire. Les valeurs numériques qui se comparent égales ont la même valeur de hachage (même si elles sont de types différents, comme c'est le cas pour 1 Et 1.0).

Deuxièmement, une limitation du hachage est signalée dans la documentation pour object.__hash__

object.__hash__(self)

Appelé par la fonction intégrée hash() et pour les opérations sur les membres des collections hachées, notamment set, frozenset et dict. __hash__() devrait renvoyer un entier. La seule propriété requise est que les objets qui se comparent égaux ont la même valeur de hachage;

Ce n'est pas unique à python. Java a la même mise en garde: si vous implémentez hashCode alors, pour que les choses fonctionnent correctement, vous devez l'implémenter de telle sorte que: x.equals(y) implique x.hashCode() == y.hashCode().

Donc, python a décidé que 1.0 == 1 Tient, donc c'est forcé pour fournir une implémentation de hash telle que hash(1.0) == hash(1). L'effet secondaire est que 1.0 Et 1 Agissent exactement de la même manière que les touches dict, d'où le comportement.

En d'autres termes, le comportement en soi ne doit en aucun cas être utilisé ou utile. C'est nécessaire . Sans ce comportement, il y aurait des cas où vous pourriez accidentellement remplacer une clé différente.

Si nous avions 1.0 == 1 Mais hash(1.0) != hash(1) nous pourrions encore avoir une collision. Et si 1.0 Et 1 Entrent en collision, le dict utilisera l'égalité pour être sûr qu'il s'agit de la même clé ou non et kaboom le la valeur est écrasée même si vous vouliez qu'ils soient différents.

La seule façon d'éviter cela serait d'avoir 1.0 != 1, Afin que le dict soit capable de les distinguer même en cas de collision. Mais il a été jugé plus important d'avoir 1.0 == 1 Que d'éviter le comportement que vous voyez, car vous n'utilisez pratiquement jamais floats et ints comme clés de dictionnaire de toute façon.

Puisque python essaie de cacher la distinction entre les nombres en les convertissant automatiquement en cas de besoin (par exemple 1/2 -> 0.5), Il est logique que ce comportement se reflète même dans de telles circonstances. Il est plus cohérent avec le reste de python.


Ce comportement apparaîtrait dans l'implémentation any où la correspondance des clés est au moins partiellement (comme dans une carte de hachage) basée sur des comparaisons.

Par exemple, si un dict a été implémenté à l'aide d'un arbre rouge-noir ou d'un autre type de BST équilibré, lorsque la clé 1.0 Est recherchée, les comparaisons avec d'autres clés renverraient les mêmes résultats que pour 1 Et ils agiraient donc toujours de la même manière.

Les cartes de hachage nécessitent encore plus de soin, car c'est la valeur du hachage qui est utilisée pour trouver l'entrée de la clé et les comparaisons ne sont effectuées qu'après. Donc, enfreindre la règle présentée ci-dessus signifie que vous introduisez un bogue assez difficile à repérer car parfois dict peut sembler fonctionner comme vous vous y attendez, et à d'autres moments, lorsque la taille change, il commencerait à se comporter incorrectement.


Notez qu'il existe serait un moyen de résoudre ce problème: avoir une carte de hachage/BST distincte pour chaque type inséré dans le dictionnaire. De cette façon, il ne pouvait pas y avoir de collision entre des objets de type différent et la comparaison entre == N'aurait pas d'importance lorsque les arguments ont des types différents.

Cependant, cela compliquerait la mise en œuvre, ce serait probablement inefficace car les cartes de hachage doivent conserver un certain nombre d'emplacements libres afin d'avoir O(1) temps d'accès. S'ils deviennent trop complets, les performances Avoir plusieurs cartes de hachage signifie perdre plus d'espace et vous devez également d'abord choisir la carte de hachage à consulter avant même de lancer la recherche réelle de la clé.

Si vous avez utilisé des BST, vous devez d'abord rechercher le type et effectuer une deuxième recherche. Donc, si vous allez utiliser de nombreux types, vous vous retrouvez avec deux fois le travail (et la recherche prend O (log n) au lieu de O (1)).

97
Bakuriu

En python:

1==1.0
True

Cela est dû au casting implicite

Toutefois:

1 is 1.0
False

Je peux voir pourquoi la conversion automatique entre float et int est pratique, il est relativement sûr de convertir int en float, et pourtant il existe d'autres langues ( par exemple aller) qui restent à l'écart de la diffusion implicite.

Il s'agit en fait d'une décision de conception du langage et d'une question de goût plus que de fonctionnalités différentes

7
Uri Goren

Les dictionnaires sont implémentés avec une table de hachage. Pour rechercher quelque chose dans une table de hachage, vous commencez à la position indiquée par la valeur de hachage, puis recherchez différents emplacements jusqu'à ce que vous trouviez une valeur de clé égale ou un compartiment vide.

Si vous avez deux valeurs de clé qui se comparent mais ont des hachages différents, vous pouvez obtenir des résultats incohérents selon que l'autre valeur de clé se trouvait dans les emplacements recherchés ou non. Par exemple, cela serait plus probable à mesure que la table se remplit. C'est quelque chose que vous voulez éviter. Il semble que les développeurs Python avaient cela à l'esprit, car la fonction intégrée hash renvoie le même hachage pour les valeurs numériques équivalentes, peu importe si ces valeurs sont int ou float. Notez que cela s'étend à d'autres types numériques, False est égal à 0 et True est égal à 1. Même fractions.Fraction Et decimal.Decimal Maintiennent cette propriété.

L'exigence que si a == b Alors hash(a) == hash(b) est documentée dans la définition de object.__hash__() :

Appelé par la fonction intégrée hash() et pour les opérations sur les membres des collections hachées, notamment set, frozenset et dict. __hash__() devrait retourner un entier. La seule propriété requise est que les objets qui se comparent égaux ont la même valeur de hachage; il est conseillé de mélanger d'une manière ou d'une autre (par exemple en utilisant exclusif ou) les valeurs de hachage pour les composants de l'objet qui jouent également un rôle dans la comparaison des objets.

TL; DR: un dictionnaire se briserait si les clés qui étaient égales ne correspondaient pas à la même valeur.

6
Mark Ransom

Franchement, le contraire est dangereux! 1 == 1.0, il n'est donc pas improbable d'imaginer que si vous les faisiez pointer vers des clés différentes et que vous tentiez d'y accéder en fonction d'un nombre évalué, vous auriez probablement des problèmes avec cela car l'ambiguïté est difficile à comprendre.

La frappe dynamique signifie que la valeur est plus importante que le type technique de quelque chose, car le type est malléable (ce qui est une fonctionnalité très utile) et distingue ainsi à la fois ints et floats de la même valeur que distinct est une sémantique inutile qui ne fera que semer la confusion.

3
SuperBiasedMan

Je suis d'accord avec les autres pour dire qu'il est logique de traiter 1 et 1.0 comme dans ce contexte. Même si Python les traitait différemment, ce serait probablement une mauvaise idée d'essayer d'utiliser 1 et 1.0 comme clés distinctes pour un dictionnaire. D'un autre côté - j'ai du mal à penser à un cas d'utilisation naturel pour utiliser 1.0 comme alias pour 1 dans le contexte des clés. Le problème est que la clé est littérale ou calculée. S'il s'agit d'une clé littérale, pourquoi ne pas simplement utiliser 1 plutôt que 1.0? S'il s'agit d'une clé calculée - une erreur d'arrondi pourrait gâcher les choses:

>>> d = {}
>>> d[1] = 5
>>> d[1.0]
5
>>> x = sum(0.01 for i in range(100)) #conceptually this is 1.0
>>> d[x]
Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    d[x]
KeyError: 1.0000000000000007

Je dirais donc que, d'une manière générale, la réponse à votre question "est-ce une fonctionnalité linguistique utile?" est "Non, probablement pas."

3
John Coleman