Numeri complessi e unit test in Python

Sep 17 2020

Ho appena iniziato un corso che richiederà conoscenza ed esperienza con Python per progetti futuri, cosa che non ho, quindi ho pensato di provarlo per familiarizzare con esso e ottenere un feedback. Ho ottenuto un discreto riepilogo delle caratteristiche principali della lingua da un video di panoramica di 2 ore (https://www.youtube.com/watch?v=H1elmMBnykA), ho provato alcune piccole cose da solo, quindi ho deciso di passare a qualcosa di un po 'più interessante.

Come indica il titolo, il codice è costituito da due classi:, Complexche rappresenta numeri complessi e ComplexTestche è una sequenza di unit test per la Complexclasse. Sono consapevole che Python supporta nativamente i numeri complessi. Dovrei anche menzionare che tutti gli unit test vengono ComplexTesteseguiti correttamente e superati.

Sono interessato a commentare letteralmente qualsiasi parte del mio codice, poiché è la prima volta che scrivo codice Python. Qualsiasi feedback è il benvenuto!

Infine, un punto che mi ha un po 'irritato è stato l'apparente scontro tra Python 2 e Python 3, che spesso mi ha reso insicuro se il modo in cui stavo implementando le cose fosse "corretto" o meno dalla prospettiva di Python 3 (che è quella che io' m targeting).

Mi mancano anche i punti e virgola e le parentesi graffe :(

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)

Risposte

3 superbrain Sep 17 2020 at 07:22

Sembra piuttosto buono.

Vedo che hai implementato il modulo in mod. Si chiama anche valore assoluto, e questo è il nome che Python usa. Se implementi __abs__, la absfunzione di Python può usarlo. Allora abs(Complex(3, 4))ti darei 5.0. Proprio come abs(3 + 4j)fa lo stesso di Python .

Un altro utile è __bool__, che ti consente di dichiarare zero come falso, come è standard in Python. Attualmente fallisci (cioè, viene stampato):

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

Puoi anche usarlo due volte all'interno del tuo __truediv__metodo. Mi piace if not self:.

Il test di (in) uguaglianza potrebbe essere esteso. Ad esempio, mi aspetto Complex(3) == 3di dare a me True, non di crash. E poi i tuoi test all'interno __truediv__potrebbero in alternativa essere come if self == 0:.

Puoi dare un'occhiata a ciò che hanno i numeri complessi di 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

Oggetti valore

Quanto segue mostra ciò che la maggior parte degli utenti considererebbe un comportamento imprevisto:

from ccomplex import Complex

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

I valori sono generalmente considerati immutabili. Poiché Python utilizza oggetti per rappresentare valori e gli oggetti hanno un'identità che può essere condivisa, la migliore pratica è quella di utilizzare oggetti immutabili quando si creano quelli che normalmente sono considerati valori. Sembra che stia modificando una stringa:

a = "Hello"
a += " world"

Ma poiché strnon implementa l' __iadd__operatore, ciò che Python fa effettivamente è interpretare l'istruzione come a = a + " world", che valuta l'espressione a + " world"e assegna il risultato (un nuovo oggetto) a a. L'identità delle amodifiche quando l' a += ...istruzione viene eseguita, poiché un oggetto diverso è memorizzato in quella variabile.

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

La rimozione dei metodi @re.settere @im.settercambierebbe la tua Complexclasse in modo che sia pubblicamente immutabile. Anche se questo è un buon inizio, nulla impedisce a qualcuno di manipolare direttamente gli interni, come a._re = 7.

Il modo più semplice per rendere questa classe veramente immutabile è ereditare da una base immutabile. Supponendo che tu stia usando almeno 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 NamedTupleclasse base crea automaticamente il costruttore per te, quindi Complex(2, 3)produce il tuo 2 + 3ivalore complesso. Se non viene fornito alcun valore im, ad esempio Complex(2), 0viene utilizzato il valore predefinito di im.

Se si desidera modificare il valore reo im, è necessario creare un nuovo oggetto.

a = Complex(-8, a.im)

oppure, utilizzando NamedTuple._replace:

a = a._replace(re=-8)

Non implementato

Il lettore astuto noterà return NotImplementedquanto sopra. Questo è un singleton magico, che è un segnale a Python per provare alternative. Per esempio, a == bpotrebbe ripiego su not a.__neq__(b), b.__eq__(a)o addirittura not b.__ne__(a).

Considera: potresti non sapere di una Matrixclasse, ma potresti conoscere la tua Complexclasse. Se qualcuno lo fa cmplx * matrix, se la tua __mul__funzione aumenta TypeError, il gioco è finito. Se invece NotImplementedviene restituito, Matrix.__rmul__può essere provato, il che potrebbe funzionare.

Vedere NotImplemented e Implementing the aritmetic operations

rtruediv

Quando si valuta a / b, si prova per primo a.__truediv__(b). Se fallisce (non è stato definito o restituisce NotImplemented), b.__rtruediv__(a)può essere provato.

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

Perché isinstance(other, Complex)mai dovrebbe essere vero? Significherebbe sia che selfè un Complex(dal momento che siamo in Complex.__rtruediv__), sia che otherè un Complex(poiché lo isinstancedice in questo scenario). Ma se è così, lo stiamo facendo Complex() / Complex(), quindi __truediv__avrebbe dovuto essere usato e __rtruediv__non avrebbe nemmeno bisogno di essere preso in considerazione.

Divisione per zero

Perché Complex(2, 2) / 0alza a ZeroDivisionErrordove come Complex(2, 2) / Complex(0, 0)alza a ValueError? Non dovrebbe alzare un ZeroDivisionError?

Il nome del test test_complex_division_raises_zero_division_exceptionnon corrisponde alla with self.assertRaises(ValueError)condizione, il che suggerisce che sapevi cosa avrebbe dovuto generare e che hai scoperto l'errore, ma hai modificato il test in modo che corrisponda alla condizione che è stata sollevata, invece di sollevare l'eccezione corretta.