Nombres complexes et tests unitaires en Python
Je viens de commencer un cours qui nécessitera des connaissances et de l'expérience avec Python pour de futurs projets, ce que je n'ai pas, alors j'ai pensé que je vais essayer de me familiariser avec lui et d'obtenir des commentaires. J'ai obtenu un résumé décent des principales fonctionnalités de la langue à partir d'une vidéo de présentation de 2 heures (https://www.youtube.com/watch?v=H1elmMBnykA), j'ai essayé quelques petites choses par moi-même, puis j'ai décidé de passer à quelque chose d'un peu plus intéressant.
Comme son titre l'indique, le code se compose de deux classes:, Complexqui représente des nombres complexes, et ComplexTestqui est une séquence de tests unitaires pour la Complexclasse. Je suis conscient que Python prend en charge les nombres complexes de manière native. Je dois également mentionner que tous les tests unitaires de ComplexTests'exécutent correctement et passent.
Je suis intéressé par des commentaires sur toutes les parties de mon code, car c'est la première fois que j'écris du code Python. Tous les commentaires sont les bienvenus!
Enfin, un point qui m'a un peu contrarié était le conflit apparent entre Python 2 et Python 3, qui me faisait souvent ne pas savoir si la façon dont j'implémentais les choses était "correcte" ou non du point de vue de Python 3 (qui est celui que je m ciblage).
Mes points-virgules et accolades me manquent aussi beaucoup :(
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)
Réponses
Ça a l'air plutôt bien.
Je vois que vous avez implémenté le module dans mod. C'est aussi appelé valeur absolue, et c'est le nom que Python utilise. Si vous implémentez __abs__, la absfonction de Python peut l'utiliser. Alors abs(Complex(3, 4))vous donnerait 5.0. Tout comme Python le abs(3 + 4j)fait.
Un autre outil utile est __bool__, qui vous permet de déclarer zéro comme faux, comme c'est le cas en Python. Actuellement, vous échouez (c'est-à-dire qu'il est imprimé):
if Complex(0, 0):
print('this should not get printed')
Vous pouvez également l'utiliser deux fois dans votre __truediv__méthode. Comme if not self:.
Le test d '(in) égalité pourrait être étendu. Par exemple, je m'attendrais Complex(3) == 3à me donner True, pas à planter. Et puis vos tests à l'intérieur __truediv__pourraient également être comme if self == 0:.
Vous pouvez jeter un œil à ce que possèdent les nombres complexes de Python:
>>> for name in dir(1j):
print(name)
__abs__
__add__
__bool__
__class__
__delattr__
__dir__
__divmod__
__doc__
__eq__
__float__
...
Objets de valeur
Ce qui suit montre ce que la plupart des utilisateurs considéreraient comme un comportement inattendu:
from ccomplex import Complex
a = Complex(5, 4) + Complex(3)
b = a
a.re = -a.re
print(b) # "-8 + 4i"
Les valeurs sont généralement considérées comme immuables. Étant donné que Python utilise des objets pour représenter des valeurs et que les objets ont une identité qui peut être partagée, la meilleure pratique consiste à utiliser des objets immuables lors de la création de valeurs normalement considérées. Cela ressemble à la modification d'une chaîne:
a = "Hello"
a += " world"
Mais comme strn'implémente pas l' __iadd__opérateur, ce que fait réellement Python est d'interpréter l'instruction comme a = a + " world", qui évalue l'expression a + " world"et affecte le résultat (un nouvel objet) à a. L'identité des achangements lors de l' a += ...exécution de l' instruction, puisqu'un objet différent est stocké dans cette variable.
>>> a = "hello"
>>> id(a)
1966355478512
>>> a += " world"
>>> id(a)
1966350779120
>>>
La suppression des méthodes @re.setteret @im.setterchangerait votre Complexclasse pour qu'elle soit publiquement immuable. Bien que ce soit un bon début, rien n'empêche quelqu'un de manipuler directement les composants internes, comme a._re = 7.
Le moyen le plus simple de rendre cette classe vraiment immuable est d'hériter d'une base immuable. En supposant que vous utilisez au moins 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 de base crée automatiquement le constructeur pour vous, donc Complex(2, 3)produit votre 2 + 3ivaleur complexe. Si aucune valeur n'est fournie pour im, par exemple Complex(2), la valeur par défaut de 0est utilisée pour im.
Si vous souhaitez modifier la valeur reou im, vous devez créer un nouvel objet.
a = Complex(-8, a.im)
ou, en utilisant NamedTuple._replace:
a = a._replace(re=-8)
Pas mis en œuvre
Le lecteur avisé remarquera return NotImplementedce qui précède. Il s'agit d'un singleton magique, qui indique à Python d'essayer des alternatives. Par exemple, a == bpourrait fallback sur not a.__neq__(b), b.__eq__(a)ou même not b.__ne__(a).
Considérez ceci: vous ne connaissez peut-être pas une Matrixclasse, mais elle peut connaître votre Complexclasse. Si quelqu'un le fait cmplx * matrix, si votre __mul__fonction augmente TypeError, la partie est terminée. Si à la place NotImplementedest retourné, alors Matrix.__rmul__peut être essayé, ce qui pourrait fonctionner.
Voir NotImplemented et Implémentation des opérations arithmétiques
rtruediv
Lors de l'évaluation a / b, le premier est essayé a.__truediv__(b). Si cela échoue (n'a pas été défini ou retourne NotImplemented), b.__rtruediv__(a)peut être essayé.
class Complex:
...
def __rtruediv__(self, other):
if isinstance(other, Complex):
...
...
Pourquoi serait-ce isinstance(other, Complex)jamais vrai? Cela signifierait à la fois que selfc'est un Complex(puisque nous sommes dedans Complex.__rtruediv__) et otherest un Complex(puisque le isinstancedit dans ce scénario). Mais si tel est le cas, nous le faisons Complex() / Complex(), alors cela __truediv__aurait dû être utilisé et __rtruediv__n'aurait même pas besoin d'être pris en compte.
Division par zéro
Pourquoi est-ce que Complex(2, 2) / 0soulève un ZeroDivisionErroroù comme Complex(2, 2) / Complex(0, 0)soulève un ValueError? Ne devrait-il pas augmenter un ZeroDivisionError?
Le nom de votre test test_complex_division_raises_zero_division_exceptionne correspond pas à la with self.assertRaises(ValueError)condition, ce qui suggère que vous saviez ce qu'il aurait dû déclencher et que vous avez découvert l'erreur, mais que vous avez modifié le test pour qu'il corresponde à la condition qui a été déclenchée, au lieu de déclencher l'exception correcte.