如何对浮点数和复数进行近似结构模式匹配

Ray*_*ger 55 python floating-point complex-numbers approximate structural-pattern-matching

我读过并理解浮点舍入问题,例如:

>>> sum([0.1] * 10) == 1.0
False

>>> 1.1 + 2.2 == 3.3
False

>>> sin(radians(45)) == sqrt(2) / 2
False
Run Code Online (Sandbox Code Playgroud)

我还知道如何使用math.isclose()cmath.isclose()解决这些问题。

问题是如何将这些解决方法应用到 Python 的 match/case 语句中。我希望这个工作:

match 1.1 + 2.2:
    case 3.3:
        print('hit!')  # currently, this doesn't match
Run Code Online (Sandbox Code Playgroud)

Ray*_*ger 59

解决方案的关键是构建一个包装器来重写该__eq__方法并将其替换为近似匹配:

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
Run Code Online (Sandbox Code Playgroud)

它为浮点值和复数值创建近似相等测试:

>>> 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
Run Code Online (Sandbox Code Playgroud)

以下是如何在 match/case 语句中使用它:

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')
Run Code Online (Sandbox Code Playgroud)

这输出:

0.9999999999999999 sums to about 1.0
3.3000000000000003 sums to about 3.3
0.7071067811865475 is close to sqrt(2) / 2
Run Code Online (Sandbox Code Playgroud)

  • 由于时间戳是浮点数,因此这也应该适用于近似时间。 (2认同)
  • 小提示:在`__eq__`中需要进行某种类型检查,以使其在混合类型的`match`中工作,例如添加`if not isinstance(other,numbers.Complex): return NotImplemented`(`Complex ` 是 `Real` 的超类,因此它支持除 `decimal.Decimal` 之外的大多数数字类型,这是一种奇怪的情况),因此当输入可能是非数字时,它不会因 `TypeError` 而终止(因为其他“case”旨在捕获非数字内容)。 (2认同)

Sam*_*son 29

雷蒙德的答案非常奇特且符合人体工程学,但对于一些可以简单得多的事情来说似乎有很多魔力。一个更简单的版本只是捕获计算值并明确检查事物是否“接近”,例如:

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)
Run Code Online (Sandbox Code Playgroud)

我还建议仅在cmath.isclose()您实际需要的地方/时间使用它,使用适当的类型可以确保您的代码按照您的预期进行。

上面的示例只是用于演示匹配的最少代码,并且正如注释中指出的那样,使用传统if语句可以更轻松地实现。冒着偏离原始问题的风险,这是一个更完整的示例:

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)))
Run Code Online (Sandbox Code Playgroud)

不确定我是否真的会match在这里使用一个声明,但希望更具代表性!

  • @RaymondHettinger 当然,它可以用 `if-elif` 代替,但为什么要这么做呢? (2认同)