52
votes

Comment effectuer une correspondance de motifs structurels approximatifs pour les flotteurs et complexes

J'ai lu et compris Problèmes d'arrondage des points flottants AS:

match 1.1 + 2.2:
    case 3.3:
        print('hit!')  # currently, this doesn't match


4 commentaires

Soyez averti que *. Isclose sont une heuristique et peuvent eux-mêmes échouer de manière inattendue.


Par exemple, math.isclose (a, b) et math.isclose (b, c) mais pas math.isclose (a, c) . (Par exemple avec les paramètres par défaut, a, b, c = 1, 1,0000000005, 1.0000000015 )


Les fonctions isClose () ont des définitions précises et sont complètement contrôlables. Ils sont offerts par la bibliothèque standard comme moyen accepté de faire des comparaisons pour les chars à proximité. Personne n'a fait affirmer selon lequel Isclose () est transitif et cela n'est pas pertinent pour ce cas d'utilisation - c'est un hareng rouge pour créer un sentiment d'inquiétude vague et inaccessible. S'il y a une meilleure solution, veuillez le poster.


Je crois que c'est proche de cette question stackoverflow.com/questions/51018201/…


2 Réponses :


54
votes

La clé de la solution consiste à construire un wrapper qui remplace la méthode __ eq __ et le remplace par une correspondance approximative:

0.9999999999999999 sums to about 1.0
3.3000000000000003 sums to about 3.3
0.7071067811865475 is close to sqrt(2) / 2

Il crée des tests d'égalité approximatifs pour Les deux valeurs flottantes et les valeurs complexes:

for x in [sum([0.1] * 10), 1.1 + 2.2, sin(radians(45))]:
    match Approximately(x):
        case 1.0:
            print(x, 'sums to about 1.0')
        case 3.3:
            print(x, 'sums to about 3.3')
        case 0.7071067811865475:
            print(x, 'is close to sqrt(2) / 2')
        case _:
            print('Mismatch')

Voici comment l'utiliser dans une instruction MATCH / Case:

>>> Approximately(1.1 + 2.2) == 3.3
True
>>> Approximately(1.1 + 2.2, abs_tol=0.2) == 3.4
True
>>> Approximately(1.1j + 2.2j) == 0.0 + 3.3j
True

Cela sort:

import cmath

class Approximately(complex):

    def __new__(cls, x, /, **kwargs):
        result = complex.__new__(cls, x)
        result.kwargs = kwargs
        return result

    def __eq__(self, other):
        try:
            return isclose(self, other, **self.kwargs)
        except TypeError:
            return NotImplemented


2 commentaires

Étant donné que les horodatages sont des flotteurs, cela devrait également fonctionner bien pour les temps approximatifs.


Remarque mineure: un type de type de type serait nécessaire dans __ eq __ pour que cela fonctionne dans le type de type mixte ES, par ex. Ajouter sinon IsInstance (Autre, nombres.Complex): return notImpléted ( complexe est une superclasse de réel , donc il prendrait en charge la plupart des types numériques Mis à part decimal.decimal , qui est un cas bizarre), il ne mourrait pas avec un TypeError lorsque l'entrée peut être non nucère (car un autre cas sont destinés à attraper des trucs non numériques).



28
votes

La réponse de Raymond est très sophistiquée et ergonomique, mais semble être beaucoup de magie pour quelque chose qui pourrait être beaucoup plus simple. Une version plus minimale serait simplement de capturer la valeur calculée et de vérifier explicitement si les choses sont "fermées", par exemple :

from dataclasses import dataclass

@dataclass
class Square:
    size: float

@dataclass
class Rectangle:
    width: float
    height: float

def classify(obj: Square | Rectangle) -> str:
    match obj:
        case Square(size=x) if math.isclose(x, 1):
            return "~unit square"

        case Square(size=x):
            return f"square, size={x}"

        case Rectangle(width=w, height=h) if math.isclose(w, h):
            return "~square rectangle"

        case Rectangle(width=w, height=h):
            return f"rectangle, width={w}, height={h}"

almost_one = 1 + 1e-10
print(classify(Square(almost_one)))
print(classify(Rectangle(1, almost_one)))
print(classify(Rectangle(1, 2)))

Je suggère également d'utiliser cmath.isclose () où / Lorsque vous en avez réellement besoin, en utilisant les types appropriés, vous permet de vous assurer que votre code fait ce que vous attendez.

L'exemple ci-dessus est juste le code minimum utilisé pour démontrer la correspondance et, Comme indiqué dans les commentaires, pourrait être plus facilement implémenté à l'aide d'une instruction IF traditionnelle. Au risque de faire dérailler la question d'origine, il s'agit d'un exemple un peu plus complet:

import math

match 1.1 + 2.2:
    case x if math.isclose(x, 3.3):
        print(f"{x} is close to 3.3")
    case x:
        print(f"{x} wasn't close)

Je ne sais pas si j'utiliserais réellement une instruction Match Ici, mais espérons-le, plus représentatif!


5 commentaires

@RaymondHettinger Pouvez-vous expliquer un peu plus? Je pensais que l'utilisation d'une correspondance / cas vs if / elif dépend de la sémantique que vous essayez d'implémenter. Pourquoi l'expression correspondante est-elle importante dans cette décision? Est-ce une chose spécifique à Python ou une idée générale dont vous parlez?


@RaymondHettinger: Lorsque vous dites " toujours faire mieux avec la chaîne if-elif-else", que voulez-vous dire? Votre solution (que j'ai votée à vote) réduit la duplication de code en échange d'un peu plus magique à distance, mais en termes de performances, ils devraient être essentiellement identiques (hypothétique basée sur le hachage correspond à es «Il optimisera non plus; cela sera toujours équivalent à if-elif-else sous le capot). Vous pouvez toujours passer une valeur calculée à la Match sans la prétendre dans une variable, vous pouvez toujours mettre théoriquement à cas pour d'autres types, etc. Suis-je en train de manquer quelque chose Cela aggrave cela fondamentalement?


@RaymondHettinger: serait-il mieux avec plusieurs modèles case x si math.isclose (x, some_float) ? En l'état, il pourrait en effet être complètement remplacé par si math.isclose (1.1 + 2.2, 3.3): .


@RaymondHettinger Bien sûr, il peut être substitué par if-elif , mais pourquoi le faire?


@BarryBostWick Match / Case avec uniquement des gardes est plus vertige, plus imbriqué, moins clair et probablement plus lent qu'un équivalent de chaîne if-elif-else.