Números complejos y pruebas unitarias en Python

Sep 17 2020

Acabo de comenzar un curso que requerirá conocimiento y experiencia con Python para proyectos futuros, que no tengo, así que pensé en darle una oportunidad para familiarizarme con él y obtener algunos comentarios. Obtuve un resumen decente de las características principales del idioma de un video de descripción general de 2 horas (https://www.youtube.com/watch?v=H1elmMBnykA), probé algunas cosas pequeñas por mi cuenta, luego decidí pasar a algo un poco más interesante.

Como indica el título, el código consta de dos clases:, Complexque representa números complejos y ComplexTestque es una secuencia de pruebas unitarias para la Complexclase. Soy consciente de que Python admite números complejos de forma nativa. También debo mencionar que todas las pruebas unitarias se ComplexTestejecutan correctamente y pasan.

Estoy interesado en comentar sobre literalmente cualquier parte de mi código, ya que esta es la primera vez que escribo código Python. ¡Todos y cada uno de los comentarios son bienvenidos!

Finalmente, un punto que me molestó un poco fue el aparente choque entre Python 2 y Python 3, que a menudo me hacía inseguro de si la forma en que estaba implementando las cosas era "correcta" o no desde la perspectiva de Python 3 (que es la que yo ' m focalización).

También echo mucho de menos mis puntos y comas y llaves :(

ccomplex.py

from numbers import Number
import math


class Complex:
    def __init__(self, re=0, im=0):
        self._re = re
        self._im = im

    def __eq__(self, other):
        if isinstance(other, Complex):
            return self.re == other.re and self.im == other.im
        else:
            raise TypeError("The argument should be an instance of Complex")

    def __neg__(self):
        return Complex(-self.re, -self.im)

    def __add__(self, other):
        if isinstance(other, Complex):
            return Complex(self.re + other.re, self.im + other.im)
        else:
            raise TypeError("The argument should be an instance of Complex")

    def __sub__(self, other):
        if isinstance(other, Complex):
            return self + (-other)
        else:
            raise TypeError("The argument should be an instance of Complex")

    def __mul__(self, other):
        if isinstance(other, Complex):
            a = self.re * other.re - self.im * other.im
            b = self.re * other.im + self.im * other.re
            return Complex(a, b)
        elif isinstance(other, Number):
            return Complex(self.re * other, self.im * other)
        else:
            raise TypeError(
                "The argument should be an instance of Complex or Number")

    def __rmul__(self, other):
        return self * other

    def __truediv__(self, other):
        if isinstance(other, Complex):
            if self.re == 0 and self.im == 0:
                return Complex(0, 0)

            if other.re == 0 and other.im == 0:
                raise ValueError("The argument should be different from zero")

            return (self * other.conj()) / other.mod_squared()
        elif isinstance(other, Number):
            return Complex(self.re / other, self.im / other)
        else:
            raise TypeError(
                "The argument should be an instance of Complex or Number")

    def __rtruediv__(self, other):
        if isinstance(other, Complex):
            if other.re == 0 and other.im == 0:
                return Complex(0, 0)

            if self.re == 0 and self.im == 0:
                raise ValueError("The argument should be different from zero")

            return (other * self.conj()) / self.mod_squared()
        elif isinstance(other, Number):
            return Complex(other, 0) / self
        else:
            raise TypeError(
                "The argument should be an instance of Complex or Number")

    def conj(self):
        return Complex(self.re, -self.im)

    def mod_squared(self):
        return self.re * self.re + self.im * self.im

    def mod(self):
        return math.sqrt(self.mod_squared())

    def arg(self):
        return math.atan2(self.im, self.re)

    @property
    def re(self):
        return self._re

    @re.setter
    def re(self, value):
        self._re = value

    @property
    def im(self):
        return self._im

    @im.setter
    def im(self, value):
        self._im = value

    def __str__(self):
        op = "+" if self.im >= 0 else "-"
        return "{} {} {}i".format(self.re, op, abs(self.im))

complexTest.py

from ccomplex import Complex
import math
import unittest


class ComplexTest(unittest.TestCase):
    def test_equality(self):
        self.assertTrue(Complex(2, 2) == Complex(2, 2))

    def test_inequality(self):
        self.assertFalse(Complex(1, 1) == Complex(2, 2))

    def test_equality_raises_type_exception(self):
        with self.assertRaises(TypeError):
            z = Complex(2, 2) == "Not A Complex"

    def test_negation(self):
        self.assertEqual(-Complex(4, 4), Complex(-4, -4))

    def test_sum(self):
        z = Complex(2, 2)
        self.assertEqual(z + z, Complex(4, 4))

    def test_difference(self):
        z = Complex(4, 4)
        self.assertEqual(z - Complex(2, 2), Complex(2, 2))

    def test_complex_product(self):
        z1 = Complex(4, 4)
        z2 = Complex(2, 2)
        self.assertEqual(z1 * z2, Complex(0, 16))

    def test_product_raises_type_exception(self):
        with self.assertRaises(TypeError):
            z = Complex(2, 2) * "Not A Complex"

    def test_left_real_product(self):
        z = Complex(2, 2)
        self.assertEqual(z * 2, Complex(4, 4))

    def test_right_real_product(self):
        z = Complex(2, 2)
        self.assertEqual(2 * z, Complex(4, 4))

    def test_complex_division(self):
        z1 = Complex(4, 4)
        z2 = Complex(2, 2)
        self.assertEqual(z1 / z2, Complex(2, 0))

    def test_division_raises_type_exception(self):
        with self.assertRaises(TypeError):
            z = Complex(2, 2) / "Not A Complex"

    def test_complex_division_raises_zero_division_exception(self):
        with self.assertRaises(ValueError):
            z = Complex(2, 2) / Complex(0, 0)

    def test_real_division_raises_zero_division_exception(self):
        with self.assertRaises(ZeroDivisionError):
            z = Complex(2, 2) / 0

    def test_left_real_division(self):
        z = Complex(4, 4)
        self.assertEqual(z / 2, Complex(2, 2))

    def test_right_real_division(self):
        z = Complex(2, 2)
        self.assertEqual(2 / z, Complex(0.5, -0.5))

    def test_conjugate(self):
        z = Complex(2, 2)
        self.assertEqual(z.conj(), Complex(2, -2))

    def test_mod_squared(self):
        z = Complex(2, 2)
        self.assertAlmostEqual(z.mod_squared(), 8, delta=10e-16)

    def test_mod(self):
        z = Complex(2, 2)
        self.assertAlmostEqual(z.mod(), 2 * math.sqrt(2), delta=10e-16)

    def test_arg(self):
        z = Complex(2, 2)
        self.assertAlmostEqual(z.arg(), math.pi / 4, delta=10e-16)


if __name__ == '__main__':
    unittest.main(verbosity=2)

Respuestas

3 superbrain Sep 17 2020 at 07:22

Se ve bastante bien.

Veo que implementó el módulo en mod. También se llama valor absoluto, y ese es el nombre que usa Python. Si lo implementa __abs__, la absfunción de Python puede usarlo. Entonces abs(Complex(3, 4))te daría 5.0. Al igual que hace el propio Python abs(3 + 4j).

Otro útil es __bool__, que le permite declarar cero como falso, como es estándar en Python. Actualmente falla esto (es decir, se imprime):

if Complex(0, 0):
    print('this should not get printed')

Luego, también puede usar eso dos veces dentro de su __truediv__método. Al igual if not self:.

La prueba de (in) igualdad podría ampliarse. Por ejemplo, esperaría Complex(3) == 3darme True, no chocar. Y luego tus pruebas internas __truediv__podrían ser como alternativa if self == 0:.

Puede echar un vistazo a lo que tienen los propios números complejos de Python:

>>> for name in dir(1j):
        print(name)

__abs__
__add__
__bool__
__class__
__delattr__
__dir__
__divmod__
__doc__
__eq__
__float__
...
3 AJNeufeld Sep 18 2020 at 04:49

Objetos de valor

A continuación, se muestra lo que la mayoría de los usuarios considerarían un comportamiento inesperado:

from ccomplex import Complex

a = Complex(5, 4) + Complex(3)
b = a
a.re = -a.re
print(b)  # "-8 + 4i"

Los valores generalmente se consideran inmutables. Dado que Python usa objetos para representar valores y los objetos tienen una identidad que se puede compartir, la mejor práctica es usar objetos inmutables al crear lo que normalmente se consideran valores. Parece que está modificando una cadena:

a = "Hello"
a += " world"

Pero dado strque no implementa el __iadd__operador, lo que Python realmente hace es interpretar la declaración como a = a + " world", que evalúa la expresión a + " world"y asigna el resultado (un nuevo objeto) a a. La identidad de los acambios a medida a += ...que se ejecuta la declaración, ya que un objeto diferente se almacena en esa variable.

>>> a = "hello"
>>> id(a)
1966355478512
>>> a += " world"
>>> id(a)
1966350779120
>>> 

Eliminar los métodos @re.settery @im.settercambiaría su Complexclase para que sea públicamente inmutable. Si bien ese es un buen comienzo, nada impide que alguien manipule los componentes internos directamente, como a._re = 7.

La forma más fácil de hacer que esta clase sea verdaderamente inmutable es heredar de una base inmutable. Suponiendo que está usando al menos Python 3.7:

from typing import NamedTuple

class Complex(NamedTuple):
    re: float
    im: float = 0

    def __eq__(self, other):
        if isinstance(other, Complex):
            return self.re == other.re and self.im == other.im
        else:
            return NotImplemented

     # ... etc ...

La NamedTupleclase base crea automáticamente el constructor para usted, por lo que Complex(2, 3)produce su 2 + 3ivalor complejo. Si no se proporciona ningún valor im, como Complex(2), 0se utiliza el valor predeterminado de im.

Si desea cambiar el valor reo im, debe crear un nuevo objeto.

a = Complex(-8, a.im)

o, usando NamedTuple._replace:

a = a._replace(re=-8)

No se ha implementado

El lector astuto se dará cuenta return NotImplementedde lo anterior. Este es un singleton mágico, que es una señal para que Python pruebe alternativas. Por ejemplo, a == bpodría repliegue sobre not a.__neq__(b), b.__eq__(a)o incluso not b.__ne__(a).

Considere: es posible que no sepa sobre una Matrixclase, pero es posible que ella sepa sobre su Complexclase. Si alguien lo hace cmplx * matrix, si su __mul__función aumenta TypeError, se acabó el juego. Si, en cambio, NotImplementedse devuelve, se Matrix.__rmul__puede intentar, lo que podría funcionar.

Consulte NotImplemented e Implementación de operaciones aritméticas.

rtruediv

Al evaluar a / b, primero se intenta a.__truediv__(b). Si eso falla (no se definió o regresa NotImplemented), b.__rtruediv__(a)se puede intentar.

class Complex:
    ...
    def __rtruediv__(self, other):
        if isinstance(other, Complex):
            ...
        ...

¿Por qué isinstance(other, Complex)alguna vez sería verdad? Significaría tanto que selfes un Complex(ya que estamos adentro Complex.__rtruediv__) como que otheres un Complex(ya isinstanceque lo dice en este escenario). Pero si ese es el caso, lo estamos haciendo Complex() / Complex(), entonces __truediv__debería haberse utilizado y __rtruediv__ni siquiera sería necesario considerarlo.

División por cero

¿Por qué Complex(2, 2) / 0sube un ZeroDivisionErrordonde como Complex(2, 2) / Complex(0, 0)sube un ValueError? ¿No debería estar levantando un ZeroDivisionError?

Su nombre de prueba test_complex_division_raises_zero_division_exceptionno coincide con la with self.assertRaises(ValueError)condición, lo que sugiere que sabía lo que debería haber generado y descubrió el error, pero cambió la prueba para que coincida con la condición que se generó, en lugar de generar la excepción correcta.