Python의 복소수 및 단위 테스트

Sep 17 2020

저는 미래의 프로젝트를 위해 파이썬에 대한 지식과 경험이 필요한 과정을 막 시작했습니다. 저는 파이썬에 익숙해 지거나 피드백을받을 수있을 것이라고 생각했습니다. 2 시간 길이의 개요 비디오에서 언어의 주요 기능에 대한 적절한 요약을 얻었습니다 (https://www.youtube.com/watch?v=H1elmMBnykA), 혼자서 몇 가지 작은 일을 시도한 다음 좀 더 흥미로운 것으로 이동하기로 결정했습니다.

제목에서 알 수 있듯이 코드는 Complex복소수를 나타내는 ComplexTest, Complex클래스에 대한 단위 테스트 시퀀스의 두 클래스로 구성됩니다 . 파이썬이 기본적으로 복소수를 지원한다는 것을 알고 있습니다. 또한 모든 단위 테스트가 ComplexTest제대로 실행되고 통과 되었음을 언급해야합니다 .

나는 파이썬 코드를 처음 작성하는 것이기 때문에 말 그대로 코드의 모든 부분에 대한 주석에 관심이 있습니다. 모든 피드백을 환영합니다!

마지막으로, 저를 조금 괴롭혔던 점은 Python 2와 Python 3 사이의 명백한 충돌이었습니다.이 때문에 구현 방식이 Python 3 관점에서 "올바른"것인지 아닌지 확신 할 수 없었습니다. m 타겟팅).

나는 또한 내 세미콜론과 중괄호가 정말 그리워요 :(

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. 절대 값이라고도하며 파이썬이 사용하는 이름입니다. 를 구현 __abs__하면 Python의 abs함수에서 사용할 수 있습니다. 그럼 abs(Complex(3, 4))당신에게 줄 것 5.0입니다. 파이썬 자체처럼 abs(3 + 4j).

또 다른 유용한 것은 __bool__파이썬의 표준처럼 0을 거짓으로 선언 할 수있는입니다. 현재 실패합니다 (즉, 인쇄 됨).

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은 객체를 사용하여 값을 나타내고 객체는 공유 할 수있는 ID를 가지고 있으므로 일반적으로 값으로 간주되는 것을 만들 때 변경 불가능한 객체를 사용하는 것이 가장 좋습니다. 이것은 문자열을 수정하는 것처럼 보입니다.

a = "Hello"
a += " world"

그러나 연산자를 str구현하지 않기 때문에 __iadd__파이썬이 실제로하는 것은 a = a + " world"표현식을 평가 a + " world"하고 결과 (새 객체)를에 할당하는 문을로 해석하는 것 a입니다. 다른 개체가 해당 변수에 저장되기 때문에 명령문이 실행될 a때 의 변경 ID입니다 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__), 그리고 otherA는 Complex(이후 isinstance이 시나리오에서 그렇게 말했다). 그러나이 경우 우리는을 Complex() / Complex()하고 있으므로 __truediv__사용되어야 __rtruediv__하며 고려할 필요조차 없습니다.

0으로 나누기

이유는 무엇입니까 Complex(2, 2) / 0인상 a를 ZeroDivisionError같은 Complex(2, 2) / Complex(0, 0)을 제기은 ValueError? 그것은 제기해야하지 ZeroDivisionError않습니까?

테스트 이름 test_complex_division_raises_zero_division_exception이 with self.assertRaises(ValueError)조건 과 일치하지 않습니다. 즉 , 발생해야하는 사항을 알고 오류를 발견했지만 올바른 예외를 발생시키는 대신 발생한 조건과 일치하도록 테스트를 변경했습니다.