python - Cómo implementar pytest.approx() para clases de datos

CorePress2024-01-25  12

Supongamos que tengo una clase de datos de Python que me gustaría probar con pytest:

@dataclass
class ExamplePoint:
    x: float
    y: float

Con operaciones matemáticas simples todo está bien:

p1 = ExamplePoint(1,2)
p2 = ExamplePoint(0.5 + 0.5, 2)
p1 == p2   # True

Pero la aritmética de punto flotante rápidamente causa problemas:

p3 = ExamplePoint(1, math.sqrt(2) * math.sqrt(2))
p1 == p3  #  False

Usando pytest puedes solucionar esto con la función approx():

2.0 == approx(math.sqrt(2) * math.sqrt(2))  # True

No puedes simplemente extender esto a la clase de datos:

p1 = approx(p3) # Results error: TypeError: cannot make approximate comparisons to non-numeric values: ExamplePoint(x=1, y=2.0000000000000004) 

Mi solución actual para solucionar este problema es escribir una función approx() en la clase de datos como:

from dataclasses import astuple, dataclass
import pytest

@dataclass
class ExamplePoint:
    x: float
    y: float
    
    def approx(self, other):
        return astuple(self) == pytest.approx(astuple(other))
    
p1 = ExamplePoint(1,2)
p3 = ExamplePoint(1, math.sqrt(2) * math.sqrt(2))
p1.approx(p3)  # True

No me gusta esta solución porque EjemploPoint ahora depende de pytest. Eso parece incorrecto.

¿Cómo puedo extender pytest para que approx() funcione con mi clase de datos sin que la clase de datos sepa acerca de pytest?



------------------------------------

Puedes usar math.isclose(), que también te permite establecer una tolerancia para lo que consideras cercano. En el siguiente código, lo he aplicado por separado a las coordenadas x e y de tu Ejemplo de Punto fuera de tu clase de datos, pero puedes implementarlo de manera diferente:

@dataclass
class ExamplePoint:
    x: float
    y: float

p1 = ExamplePoint(1, 2)
p3 = ExamplePoint(1, math.sqrt(2) * math.sqrt(2))

print(math.isclose(p1.x, p3.x, rel_tol=0.01)) #True
print(math.isclose(p1.y, p3.y, rel_tol=0.01)) #True

ACTUALIZACIÓN: Así es como puedes incorporarlo a tu función approx:

from dataclasses import astuple, dataclass
import math
@dataclass
class ExamplePoint:
    x: float
    y: float

    def approx(self, other):
        return math.isclose(self.x,other.x, rel_tol=0.001) \
               and math.isclose(self.y,other.y, rel_tol=0.001)


p1 = ExamplePoint(1, 2)
p2 = ExamplePoint(1,1.99)
p3 = ExamplePoint(1, math.sqrt(2) * math.sqrt(2))

print(p1.approx(p2)) #False
print(p1.approx(p3)) #True

3

Lo que busco es una solución tan concisa como p1 == approx(p3)

- Dave Potts

27/03/2021 a las 22:30

Puedes definir fácilmente una función que haga eso. ¿Cuál es su criterio de proximidad: tanto x como y deben estar dentro del 0,1 % del objetivo?

-pakpe

27/03/2021 a las 22:36

Gracias @pakpe. Esto definitivamente me hizo avanzar. Personalmente, todavía me sentía incómodo por tener que implementar una función approx() y tener la sintaxis del te.st a diferencia del pytest idomático. Mi preferencia personal es mi respuesta que acabo de publicar. El tuyo claramente también funciona.

- Dave Potts

28 de marzo de 2021 a las 9:43



------------------------------------

Al profundizar en el código de pytest (consulte https://github.com/pytest-dev/pytest/blob/main/src/_pytest/python_api.py), parece que pytest comprueba si el valor esperado es iterable y considerable. . Puedo hacer que mi clase de datos sea iterable y dimensionable de la siguiente manera.

from dataclasses import astuple, dataclass
import pytest
import math

@dataclass
class ExamplePoint:
    x: float
    y: float

    def approx(self, other):
        return astuple(self) == pytest.approx(astuple(other))

    def __iter__(self):
        return iter(astuple(self))

    def __len__(self):
        return len(astuple(self))

p1 = ExamplePoint(1,2)
p3 = ExamplePoint(1, math.sqrt(2) * math.sqrt(2))

assert p1 == pytest.approx(p3)  # True

------------------------------

Debes agregar approx a cada parámetro de la clase de datos:

from dataclasses import dataclass
import pytest

@dataclass
class Foo:
    a: int
    b: float

a = Foo(1, 3.00000001)
b = Foo(1, pytest.approx(3.0, abs=1e-3))
print(a == b)

Ver: https://github.com/pytest-dev/pytest/issues/6632#issuecomment-580487103

Su guía para un futuro mejor - libreflare
Su guía para un futuro mejor - libreflare