Комплексные числа и модульные тесты в Python

Sep 17 2020

Я только начал курс, который потребует знаний и опыта работы с 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)

Ответы

3 superbrain Sep 17 2020 at 07:22

Выглядит неплохо.

Я вижу, вы реализовали модуль в 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__
...
3 AJNeufeld Sep 18 2020 at 04:49

Объекты значений

Ниже показано, что большинство пользователей сочли бы неожиданным поведением:

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)условию, что говорит о том, что вы знали, что он должен был вызвать, и обнаружили ошибку, но изменили тест в соответствии с возникшим условием вместо того, чтобы вызвать правильное исключение.