Números complexos e testes de unidade em Python

Sep 17 2020

Acabei de iniciar um curso que exigirá conhecimento e experiência com Python para projetos futuros, o que não tenho, então pensei em dar uma chance para me familiarizar com ele e obter alguns comentários. Eu obtive um resumo decente das principais características do idioma em um vídeo de visão geral de 2 horas (https://www.youtube.com/watch?v=H1elmMBnykA), tentei algumas pequenas coisas sozinho e depois decidi passar para algo um pouco mais interessante.

Como o título indica, o código consiste em duas classes:, Complexque representa números complexos e ComplexTestque é uma sequência de testes de unidade para a Complexclasse. Estou ciente de que Python oferece suporte nativo a números complexos. Devo também mencionar que todos os testes de unidade são ComplexTestexecutados corretamente e passam.

Estou interessado em comentar literalmente qualquer parte do meu código, já que esta é minha primeira vez escrevendo código Python. Todo e qualquer feedback é bem-vindo!

Finalmente, um ponto que me irritou um pouco foi o conflito aparente entre Python 2 e Python 3, que muitas vezes me deixava inseguro se a maneira como eu estava implementando as coisas era "correta" ou não da perspectiva do Python 3 (que é a mesma que eu ' segmentação m).

Eu também sinto muita falta dos meus pontos e vírgulas e chaves :(

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)

Respostas

3 superbrain Sep 17 2020 at 07:22

Parece muito bom.

Vejo que você implementou o módulo em mod. Também é chamado de valor absoluto, e esse é o nome que o Python usa. Se você implementar __abs__, a absfunção do Python pode usá-lo. Então abs(Complex(3, 4))daria a você 5.0. Assim como o próprio Python abs(3 + 4j)faz.

Outro útil é __bool__, que permite declarar zero como falso, como é padrão em Python. Atualmente você falha nisso (ou seja, é impresso):

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

Você também pode usar isso duas vezes dentro do seu __truediv__método. Gosto if not self:.

O teste de (in) igualdade pode ser estendido. Por exemplo, eu esperaria Complex(3) == 3me dar True, não travar. E então seus testes internos __truediv__podem ser alternativamente semelhantes if self == 0:.

Você pode dar uma olhada no que os próprios números complexos do Python têm:

>>> 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

O seguinte mostra o que a maioria dos usuários considera um comportamento inesperado:

from ccomplex import Complex

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

Os valores geralmente são considerados imutáveis. Como o Python usa objetos para representar valores e os objetos têm identidade que pode ser compartilhada, a prática recomendada é usar objetos imutáveis ​​ao criar o que normalmente são considerados valores. Parece que está modificando uma string:

a = "Hello"
a += " world"

Mas, como strnão implementa o __iadd__operador, o que o Python realmente faz é interpretar a instrução como a = a + " world", que avalia a expressão a + " world"e atribui o resultado (um novo objeto) a a. A identidade das amudanças conforme a a += ...instrução é executada, uma vez que um objeto diferente é armazenado nessa variável.

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

A remoção dos métodos @re.settere @im.settermudaria sua Complexclasse para ser publicamente imutável. Embora seja um bom começo, nada impede que alguém manipule os componentes internos diretamente, como a._re = 7.

A maneira mais fácil de tornar essa classe realmente imutável é herdar de uma base imutável. Supondo que você esteja usando pelo 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 ...

A NamedTupleclasse base cria automaticamente o construtor para você, portanto, Complex(2, 3)produz seu 2 + 3ivalor complexo. Se nenhum valor for fornecido para im, como Complex(2), o padrão de 0será usado para im.

Se você quiser alterar o valor reou im, deverá criar um novo objeto.

a = Complex(-8, a.im)

ou, usando NamedTuple._replace:

a = a._replace(re=-8)

Não implementado

O leitor astuto notará return NotImplementedo acima. Este é um singleton mágico, que é um sinal para o Python tentar alternativas. Por exemplo, a == bpoderia fallback em not a.__neq__(b), b.__eq__(a)ou até mesmo not b.__ne__(a).

Considere: você pode não saber sobre uma Matrixclasse, mas ele pode saber sobre sua Complexclasse. Se alguém o fizer cmplx * matrix, se sua __mul__função aumentar TypeError, o jogo acaba. Se em vez disso NotImplementedfor retornado, então Matrix.__rmul__pode ser tentado, o que pode funcionar.

Consulte NotImplemented e Implementando as operações aritméticas

rtruediv

Ao avaliar a / b, primeiro é tentado a.__truediv__(b). Se falhar (não foi definido ou retorna NotImplemented), b.__rtruediv__(a)pode ser tentado.

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

Por que isinstance(other, Complex)seria verdade? Significaria que selfé a Complex(já que estamos dentro Complex.__rtruediv__) e otheré a Complex(já que isinstancediz isso neste cenário). Mas se for esse o caso, estamos fazendo Complex() / Complex(), então __truediv__deveria ter sido usado, e __rtruediv__nem mesmo precisaria ser considerado.

Divisão por zero

Por que Complex(2, 2) / 0aumenta a ZeroDivisionErroronde como Complex(2, 2) / Complex(0, 0)aumenta a ValueError? Não deveria estar criando um ZeroDivisionError?

O nome do seu teste test_complex_division_raises_zero_division_exceptionnão corresponde à with self.assertRaises(ValueError)condição, o que sugere que você sabia o que deveria ter gerado e descobriu o erro, mas alterou o teste para corresponder à condição que foi gerada, em vez de levantar a exceção correta.