Komplexe Zahlen und Komponententests in Python

Sep 17 2020

Ich habe gerade einen Kurs begonnen, der Kenntnisse und Erfahrungen mit Python für zukünftige Projekte erfordert, die ich nicht habe. Deshalb dachte ich, ich würde es versuchen, mich damit vertraut zu machen und Feedback zu erhalten. Ich habe eine anständige Zusammenfassung der Hauptfunktionen der Sprache aus einem 2-stündigen Übersichtsvideo erhalten (https://www.youtube.com/watch?v=H1elmMBnykA), habe ein paar kleine Dinge selbst ausprobiert und mich dann entschlossen, etwas Interessanteres zu machen.

Wie der Titel schon sagt, besteht der Code aus zwei Klassen: ComplexDies stellt komplexe Zahlen dar und ComplexTestist eine Folge von Komponententests für die ComplexKlasse. Mir ist bewusst, dass Python komplexe Zahlen nativ unterstützt. Ich sollte auch erwähnen, dass alle Unit-Tests ComplexTestordnungsgemäß ausgeführt und bestanden wurden.

Ich bin daran interessiert, buchstäblich alle Teile meines Codes zu kommentieren, da ich zum ersten Mal Python-Code schreibe. Jedes Feedback ist willkommen!

Ein Punkt, der mich ein wenig ärgerte, war der offensichtliche Konflikt zwischen Python 2 und Python 3, der mich oft unsicher machte, ob die Art und Weise, wie ich Dinge implementierte, aus der Sicht von Python 3 (die ich bin) "richtig" war oder nicht. m Targeting).

Ich vermisse auch meine Semikolons und geschweiften Klammern :(

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)

Antworten

3 superbrain Sep 17 2020 at 07:22

Sieht sehr gut aus.

Ich sehe, Sie haben Modul in implementiert mod. Es wird auch als absoluter Wert bezeichnet, und das ist der Name, den Python verwendet. Wenn Sie implementieren __abs__, kann die Python- absFunktion es verwenden. Dann abs(Complex(3, 4))würde ich dir geben 5.0. Genau wie Pythons abs(3 + 4j).

Eine weitere nützliche Methode ist __bool__, mit der Sie Null als falsch deklarieren können, wie es in Python Standard ist. Derzeit scheitern Sie daran (dh es wird gedruckt):

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

Sie können das dann auch zweimal in Ihrer __truediv__Methode verwenden. Wie if not self:.

Der (In-) Gleichheitstest könnte erweitert werden. Zum Beispiel würde ich erwarten Complex(3) == 3, mich zu geben True, nicht zum Absturz zu bringen. Und dann __truediv__könnten Ihre Tests im Inneren alternativ so sein if self == 0:.

Sie können sich ansehen, was Pythons eigene komplexe Zahlen haben:

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

Wertobjekte

Das Folgende zeigt, was die meisten Benutzer als unerwartetes Verhalten betrachten würden:

from ccomplex import Complex

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

Werte werden normalerweise als unveränderlich angesehen. Da Python Objekte zur Darstellung von Werten verwendet und Objekte eine Identität haben, die gemeinsam genutzt werden kann, empfiehlt es sich, unveränderliche Objekte zu verwenden, wenn Sie Werte erstellen, die normalerweise als Werte betrachtet werden. Dies sieht so aus, als würde eine Zeichenfolge geändert:

a = "Hello"
a += " world"

Da strder __iadd__Operator jedoch nicht implementiert wird, interpretiert Python die Anweisung tatsächlich als a = a + " world", was den Ausdruck auswertet a + " world"und das Ergebnis (ein neues Objekt) zuweist a. Die Identität der aÄnderungen bei der a += ...Ausführung der Anweisung, da in dieser Variablen ein anderes Objekt gespeichert ist.

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

Das Entfernen der Methoden @re.setterund @im.setterwürde Ihre ComplexKlasse so ändern , dass sie öffentlich unveränderlich ist. Während das ein guter Anfang ist, hindert nichts jemanden daran, die Interna direkt zu manipulieren, wie a._re = 7.

Der einfachste Weg, diese Klasse wirklich unveränderlich zu machen, besteht darin, von einer unveränderlichen Basis zu erben. Angenommen, Sie verwenden mindestens 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 ...

Die NamedTupleBasisklasse erstellt automatisch den Konstruktor für Sie und Complex(2, 3)erzeugt so Ihren 2 + 3ikomplexen Wert. Wenn kein Wert angegeben ist im, z. B. Complex(2)wird der Standardwert von 0verwendet im.

Wenn Sie den Wert reoder ändern immöchten, müssen Sie ein neues Objekt erstellen.

a = Complex(-8, a.im)

oder mit NamedTuple._replace:

a = a._replace(re=-8)

Nicht implementiert

Der kluge Leser wird es oben bemerken return NotImplemented. Dies ist ein magischer Singleton, der Python signalisiert, Alternativen auszuprobieren. Zum Beispiel a == bRückfall auf könnte not a.__neq__(b), b.__eq__(a)oder sogar not b.__ne__(a).

Bedenken Sie: Sie wissen möglicherweise nichts über eine MatrixKlasse, aber sie wissen möglicherweise etwas über Ihre ComplexKlasse. Wenn jemand dies tut cmplx * matrixund Ihre __mul__Funktion erhöht wird TypeError, ist das Spiel vorbei. Wenn stattdessen NotImplementedzurückgegeben wird, Matrix.__rmul__kann versucht werden, was möglicherweise funktioniert.

Siehe NotImplemented und Implementieren der arithmetischen Operationen

rtruediv

Bei der Auswertung a / bwird zunächst versucht a.__truediv__(b). Wenn dies fehlschlägt (nicht definiert wurde oder zurückkehrt NotImplemented), b.__rtruediv__(a)kann versucht werden.

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

Warum sollte das isinstance(other, Complex)jemals wahr sein? Es würde bedeuten, dass sowohl selfa Complex(da wir in sind Complex.__rtruediv__) otherals auch a Complex(da isinstancedies in diesem Szenario so ist) ist. Aber wenn das der Fall ist, tun wir es Complex() / Complex(), dann __truediv__hätte es verwendet werden sollen und __rtruediv__müsste nicht einmal in Betracht gezogen werden.

Durch Null teilen

Warum Complex(2, 2) / 0erhöht a ZeroDivisionErrorwo wie Complex(2, 2) / Complex(0, 0)erhöht a ValueError? Sollte es nicht eine erhöhen ZeroDivisionError?

Ihr Testname test_complex_division_raises_zero_division_exceptionstimmt nicht mit der with self.assertRaises(ValueError)Bedingung überein , was darauf hindeutet, dass Sie wussten, was er hätte auslösen sollen, und den Fehler entdeckt haben, aber den Test so geändert, dass er mit der ausgelösten Bedingung übereinstimmt, anstatt die richtige Ausnahme auszulösen.