Комплексные числа и модульные тесты в Python
Я только начал курс, который потребует знаний и опыта работы с Python для будущих проектов, которых у меня нет, поэтому я решил попробовать, чтобы ознакомиться с ним и получить отзывы. Я получил достойное резюме основных функций языка из двухчасового обзорного видео (https://www.youtube.com/watch?v=H1elmMBnykA), попробовал несколько мелочей самостоятельно, а затем решил перейти к чему-то более интересному.
Как видно из названия, код состоит из двух классов:, Complexкоторый представляет комплексные числа, и ComplexTestкоторый представляет собой последовательность модульных тестов для Complexкласса. Я знаю, что Python изначально поддерживает комплексные числа. Я также должен отметить, что все модульные тесты ComplexTestвыполняются правильно и проходят.
Мне интересно прокомментировать буквально любые части моего кода, поскольку я пишу код Python впервые. Любые отзывы приветствуются!
Наконец, одним моментом, который меня немного раздражал, было очевидное столкновение между Python 2 и Python 3, из-за которого я часто не был уверен, был ли способ реализации вещей «правильным» или нет с точки зрения Python 3 (а это именно то, что я ' м нацеливания).
Еще мне очень не хватает точек с запятой и фигурных скобок :(
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)
Ответы
Выглядит неплохо.
Я вижу, вы реализовали модуль в mod. Его также называют абсолютным значением, и это имя использует Python. Если вы реализуете его __abs__, absфункция Python сможет его использовать. Тогда abs(Complex(3, 4))дам тебе 5.0. Как и собственный Python abs(3 + 4j).
Еще один полезный способ - __bool__объявить ноль ложным, как это принято в Python. В настоящее время вы не выполняете это (т.е. он печатается):
if Complex(0, 0):
print('this should not get printed')
Затем вы также можете использовать это дважды в своем __truediv__методе. Нравится if not self:.
Проверка (не) равенства может быть расширена. Например, я бы ожидал Complex(3) == 3дать мне True, а не крах. И тогда ваши тесты внутри __truediv__могут как альтернативно походить if self == 0:.
Вы можете посмотреть, что есть у собственных комплексных чисел Python:
>>> for name in dir(1j):
print(name)
__abs__
__add__
__bool__
__class__
__delattr__
__dir__
__divmod__
__doc__
__eq__
__float__
...
Объекты значений
Ниже показано, что большинство пользователей сочли бы неожиданным поведением:
from ccomplex import Complex
a = Complex(5, 4) + Complex(3)
b = a
a.re = -a.re
print(b) # "-8 + 4i"
Обычно значения считаются неизменными. Поскольку Python использует объекты для представления значений, а объекты имеют идентичность, которую можно совместно использовать, рекомендуется использовать неизменяемые объекты при создании того, что обычно считается значениями. Это похоже на изменение строки:
a = "Hello"
a += " world"
Но поскольку strон не реализует __iadd__оператор, на самом деле Python интерпретирует оператор как a = a + " world", который оценивает выражение a + " world"и присваивает результат (новый объект) a. Идентичность aизменений при выполнении a += ...оператора, поскольку в этой переменной хранится другой объект.
>>> a = "hello"
>>> id(a)
1966355478512
>>> a += " world"
>>> id(a)
1966350779120
>>>
Удаление @re.setterи @im.setterметоды изменит свой Complexкласс , чтобы быть публично неизменны. Хотя это хорошее начало, ничто не мешает кому-то напрямую манипулировать внутренним устройством, например a._re = 7.
Самый простой способ сделать этот класс действительно неизменяемым - унаследовать его от неизменяемой базы. Предполагая, что вы используете как минимум 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 ...
NamedTupleБазовый класс автоматически создает конструктор для вас, так что Complex(2, 3)производит ваше 2 + 3iкомплексное значение. Если значение не предусмотрено im, например Complex(2), по умолчанию 0используется для im.
Если вы хотите изменить значение reили im, вы должны создать новый объект.
a = Complex(-8, a.im)
или, используя NamedTuple._replace:
a = a._replace(re=-8)
Не реализована
Проницательный читатель заметит return NotImplementedв вышесказанном. Это волшебный синглтон, который является сигналом для Python попробовать альтернативы. Например, a == bможет Откат на not a.__neq__(b), b.__eq__(a)или даже not b.__ne__(a).
Учтите: вы можете не знать о Matrixклассе, но он может знать о вашем Complexклассе. Если кто-то это сделает cmplx * matrix, если ваша __mul__функция повысится TypeError, игра окончена. Если вместо этого NotImplementedвозвращается, то Matrix.__rmul__можно попробовать, что может сработать.
Смотрите NotImplemented и Реализация арифметических операций
rtruediv
При оценке a / bсначала пробуется a.__truediv__(b). Если это не удается (не было определено или возвращается NotImplemented), b.__rtruediv__(a)можно попробовать.
class Complex:
...
def __rtruediv__(self, other):
if isinstance(other, Complex):
...
...
Почему это могло isinstance(other, Complex)быть правдой? Это будет означать как то, что selfэто Complex(так как мы находимся Complex.__rtruediv__), и otherэто Complex(так как это isinstanceговорит в этом случае). Но если это так, мы это делаем Complex() / Complex(), значит, тогда __truediv__следовало бы использовать, и __rtruediv__даже не нужно было бы его рассматривать.
Деление на ноль
Почему Complex(2, 2) / 0рейзит, а ZeroDivisionErrorкак Complex(2, 2) / Complex(0, 0)повышает ValueError? Разве это не должно быть повышение ZeroDivisionError?
Имя вашего теста test_complex_division_raises_zero_division_exceptionне соответствует with self.assertRaises(ValueError)условию, что говорит о том, что вы знали, что он должен был вызвать, и обнаружили ошибку, но изменили тест в соответствии с возникшим условием вместо того, чтобы вызвать правильное исключение.