Disons que nous avons la classe Python suivante (le problème existe en Java tout comme avec equals
et hashCode
)
class Temperature:
def __init__(self, degrees):
self.degrees = degrees
où degrees
est la température en Kelvin sous forme de flotteur. Maintenant, je voudrais implémenter le test d'égalité et le hachage d' Temperature
une manière qui
- compare les flottants à une différence epsilon au lieu de tests d'égalité directs,
- et honore le contrat que cela
a == b
impliquehash(a) == hash(b)
.
def __eq__(self, other):
return abs(self.degrees - other.degrees) < EPSILON
def __hash__(self):
return # What goes here?
La documentation Python parle un peu de hachage des nombres pour s'assurer que, hash(2) == hash(2.0)
mais ce n'est pas tout à fait le même problème.
Suis-je même sur la bonne voie? Et si c'est le cas, quelle est la méthode standard pour implémenter le hachage dans cette situation?
Mise à jour : Je comprends maintenant que ce type de test d'égalité pour les flottants élimine la transitivité de ==
et equals
. Mais comment cela va-t-il de pair avec la «connaissance commune» selon laquelle les flotteurs ne devraient pas être comparés directement? Si vous implémentez un opérateur d'égalité en comparant les flottants, les outils d'analyse statique se plaindront. Ont-ils raison de le faire?
la source
kelvin
?Réponses:
L'égalité floue viole les exigences que Java impose à la
equals
méthode, à savoir la transitivité , c'est-à-dire que six == y
ety == z
, alorsx == z
. Mais si vous faites une égalité floue avec, par exemple, un epsilon de 0,1, alors0.1 == 0.2
et0.2 == 0.3
, mais0.1 == 0.3
ne tient pas.Bien que Python ne documente pas une telle exigence, les implications d'avoir une égalité non transitive en font une très mauvaise idée; le raisonnement sur de tels types induit des maux de tête.
Je vous recommande donc fortement de ne pas le faire.
Soit fournir une égalité exacte et baser votre hachage sur cela de manière évidente, et fournir une méthode distincte pour effectuer la correspondance floue, soit opter pour l'approche de classe d'équivalence suggérée par Kain. Bien que dans ce dernier cas, je vous recommande de fixer votre valeur à un membre représentatif de la classe d'équivalence dans le constructeur, puis de procéder avec une égalité exacte simple et un hachage pour le reste; il est beaucoup plus facile de raisonner sur les types de cette façon.
(Mais si vous faites cela, vous pourriez tout aussi bien utiliser une représentation à virgule fixe au lieu de virgule flottante, c'est-à-dire que vous utilisez un entier pour compter les millièmes de degré, ou la précision que vous souhaitez.)
la source
==
devrait «infecter» les==
types les contenant. Autrement dit, s'ils suivent votre conseil de fournir une égalité exacte, leur outil d'analyse statique doit en outre être configuré pour avertir lorsque l'égalité est utiliséeTemperature
. C'est vraiment la seule chose que vous puissiez faire.float approximation
champ auquel il ne participe pas==
. De plus, l'outil d'analyse statique donnera déjà un avertissement dans l'==
implémentation des classes lorsque l'un des membres comparés est unfloat
type.float
champ auquel il ne participe pas==
, ne configurez pas votre outil pour avertir==
sur cette classe. Si la classe le fait, alors le marquage probable de la classe==
comme «trop exact» fera que l'outil ignorera ce type d'erreur dans l'implémentation. Par exemple, en Java, si@Deprecated void foo()
, alorsvoid bar() { foo(); }
est un avertissement, mais@Deprecated void bar() { foo(); }
ne l'est pas. Peut-être que de nombreux outils ne prennent pas en charge cela, mais certains le pourraient.Bonne chance
Vous n'allez pas pouvoir y arriver, sans être stupide avec des hachages, ou sacrifier l'epsilon.
Exemple:
Supposons que chaque point soit haché selon sa propre valeur de hachage unique.
Comme les nombres à virgule flottante sont séquentiels, il y aura jusqu'à k nombres avant une valeur en virgule flottante donnée, et jusqu'à k nombres après une valeur en virgule flottante donnée qui se trouvent dans un epsilon du point donné.
Pour chacun des deux points dans epsilon l'un de l'autre qui ne partagent pas la même valeur de hachage.
Il y a quelques cas où cela ne sera pas vrai:
Cependant> = 99% de la plage en virgule flottante hachera à une valeur unique pour toute valeur d'Epsilon qui comprend au moins une valeur en virgule flottante au-dessus ou en dessous d'une valeur en virgule flottante donnée.
Résultat
Soit> = 99% de la plage de virgule flottante entière hachée à une seule valeur, ce qui compromet sérieusement l'intention d'une valeur de hachage (et tout appareil / conteneur reposant sur un hachage à faible collision assez distribué).
Ou l'epsilon est tel que seules les correspondances exactes sont autorisées.
Granulaire
Vous pouvez bien sûr opter pour une approche granulaire à la place.
Avec cette approche, vous définissez des compartiments exacts jusqu'à une résolution particulière. c'est à dire:
Chaque compartiment a un hachage unique, et tout point flottant dans le compartiment est égal à tout autre flottant du même compartiment.
Malheureusement, il est toujours possible que deux flotteurs soient à une distance de epsilon et ont deux hachages séparés.
la source
Vous pouvez modéliser votre température sous forme d'entier sous le capot. La température a une limite inférieure naturelle (-273,15 Celsius). Donc, double (-273.15 est égal à 0 pour votre entier sous-jacent). Le deuxième élément dont vous avez besoin est la granularité de votre mappage. Vous utilisez déjà implicitement cette granularité; c'est votre EPSILON.
Il suffit de diviser votre température par EPSILON et de prendre la parole, maintenant votre hachage et votre égal se comporteront en synchronisation. En Python 3, l'entier est illimité, EPSILON peut être plus petit si vous le souhaitez.
ATTENTION Si vous modifiez la valeur d'EPSILON et que vous avez sérialisé l'objet, ils ne seront pas compatibles!
la source
L'implémentation d'une table de hachage à virgule flottante qui peut trouver des éléments "approximativement égaux" à une clé donnée nécessitera l'utilisation de deux approches ou d'une combinaison de celles-ci:
Arrondissez chaque valeur à un incrément légèrement supérieur à la plage "floue" avant de la stocker dans la table de hachage, et lorsque vous essayez de trouver une valeur, vérifiez dans la table de hachage les valeurs arrondies au-dessus et en dessous de la valeur recherchée.
Stockez chaque élément dans la table de hachage à l'aide de clés situées au-dessus et en dessous de la valeur recherchée.
Notez que l'utilisation de l'une ou l'autre approche nécessitera probablement que les entrées de table de hachage n'identifient pas les éléments, mais plutôt les listes, car il y aura probablement plusieurs éléments associés à chaque clé. La première approche ci-dessus minimisera la taille requise de la table de hachage, mais chaque recherche d'un élément ne figurant pas dans le tableau nécessitera deux recherches de table de hachage. La deuxième approche sera rapidement en mesure d'identifier que les éléments ne sont pas dans le tableau, mais nécessitera généralement que le tableau contienne environ deux fois plus d'entrées que ce qui serait autrement requis. Si l'on essaie de trouver des objets dans l'espace 2D, il peut être utile d'utiliser une approche pour la direction X et une pour la direction Y, de sorte qu'au lieu d'avoir chaque élément stocké une fois mais nécessitant quatre opérations de requête pour chaque recherche, ou d'être pouvoir utiliser une recherche pour trouver un article mais avoir à stocker chaque article quatre fois,
la source
Vous pouvez bien sûr définir «presque égal» en supprimant par exemple les huit derniers bits de la mantisse, puis en comparant ou en hachant. Le problème est que les nombres très proches les uns des autres peuvent être différents.
Il y a une certaine confusion ici: si deux nombres à virgule flottante se comparent égaux, ils sont égaux. Pour vérifier si elles sont égales, vous utilisez "==". Parfois, vous ne voulez pas vérifier l'égalité, mais lorsque vous le faites, "==" est la voie à suivre.
la source
Ce n'est pas une réponse, mais un commentaire détaillé qui peut être utile.
J'ai travaillé sur un problème similaire, tout en utilisant MPFR (basé sur GNU MP). L'approche «bucket» telle que décrite par @ Kain0_0 semble donner des résultats acceptables, mais soyez conscient des limites mises en évidence dans cette réponse.
Je voulais ajouter que - selon ce que vous essayez de faire - l'utilisation d'un système d'algèbre informatique "exact" ( caveat emptor ) comme Mathematica peut aider à compléter ou à vérifier un programme numérique inexact. Cela vous permettra de calculer les résultats sans vous soucier de l'arrondi, par exemple,
7*√2 - 5*√2
donnera2
au lieu de2.00000001
ou similaire. Bien sûr, cela entraînera des complications supplémentaires qui peuvent ou non en valoir la peine.la source