Bilangan Kompleks & Tes Unit dengan Python
Saya baru saja memulai kursus yang membutuhkan pengetahuan dan pengalaman dengan Python untuk proyek-proyek mendatang, yang tidak saya miliki, jadi saya pikir saya akan mencobanya untuk membiasakan diri dengannya dan mendapatkan beberapa umpan balik. Saya mendapat ringkasan yang layak tentang fitur-fitur utama bahasa dari video ikhtisar berdurasi 2 jam (https://www.youtube.com/watch?v=H1elmMBnykA), mencoba beberapa hal kecil sendiri, lalu memutuskan untuk beralih ke sesuatu yang sedikit lebih menarik.
Seperti yang ditunjukkan judul, kode terdiri dari dua kelas:, Complexyang mewakili bilangan kompleks, dan ComplexTestyang merupakan urutan pengujian unit untuk Complexkelas tersebut. Saya sadar bahwa Python mendukung bilangan kompleks secara native. Saya juga harus menyebutkan bahwa semua unit test dari ComplexTestberjalan dengan baik dan lulus.
Saya tertarik untuk berkomentar tentang bagian mana pun dari kode saya, karena ini adalah pertama kalinya saya menulis kode Python. Setiap dan semua umpan balik diterima!
Akhirnya, satu hal yang sedikit membuat saya kesal adalah bentrokan nyata antara Python 2 dan Python 3, yang sering membuat saya tidak yakin apakah cara saya mengimplementasikan sesuatu adalah "benar" atau tidak dari perspektif Python 3 (yang merupakan saya ' m menargetkan).
Saya juga sangat merindukan titik koma dan tanda kurung kurawal :(
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)
Jawaban
Terlihat cukup bagus.
Saya melihat Anda menerapkan modulus dalam mod. Ini juga disebut nilai absolut, dan itulah nama yang digunakan Python. Jika Anda mengimplementasikan __abs__, maka absfungsi Python dapat menggunakannya. Lalu abs(Complex(3, 4))akan memberimu 5.0. Sama seperti milik Python abs(3 + 4j).
Yang berguna lainnya adalah __bool__, yang memungkinkan Anda mendeklarasikan nol sebagai salah, seperti standar di Python. Saat ini Anda gagal ini (yaitu, dicetak):
if Complex(0, 0):
print('this should not get printed')
Anda juga dapat menggunakannya dua kali di dalam __truediv__metode Anda . Suka if not self:.
Uji kesetaraan (dalam) dapat diperpanjang. Misalnya, saya berharap Complex(3) == 3memberi saya True, bukan crash. Dan kemudian tes Anda di dalam __truediv__bisa menjadi seperti if self == 0:.
Anda dapat melihat apa yang dimiliki bilangan kompleks Python:
>>> for name in dir(1j):
print(name)
__abs__
__add__
__bool__
__class__
__delattr__
__dir__
__divmod__
__doc__
__eq__
__float__
...
Objek Nilai
Berikut ini adalah apa yang dianggap sebagian besar pengguna sebagai perilaku tidak terduga:
from ccomplex import Complex
a = Complex(5, 4) + Complex(3)
b = a
a.re = -a.re
print(b) # "-8 + 4i"
Nilai biasanya dianggap tidak berubah. Karena Python menggunakan objek untuk mewakili nilai, dan objek memiliki identitas yang dapat dibagikan, praktik terbaiknya adalah menggunakan objek yang tidak dapat diubah saat membuat apa yang biasanya dianggap nilai. Ini sepertinya sedang memodifikasi string:
a = "Hello"
a += " world"
Tetapi karena strtidak mengimplementasikan __iadd__operator, apa yang sebenarnya dilakukan Python adalah menafsirkan pernyataan sebagai a = a + " world", yang mengevaluasi ekspresi a + " world", dan menetapkan hasilnya (objek baru) ke a. Identitas aperubahan saat a += ...pernyataan dijalankan, karena objek berbeda disimpan dalam variabel itu.
>>> a = "hello"
>>> id(a)
1966355478512
>>> a += " world"
>>> id(a)
1966350779120
>>>
Menghapus @re.setterdan @im.settermetode akan mengubah Complexkelas Anda menjadi tidak dapat diubah secara publik. Meskipun itu adalah awal yang baik, tidak ada yang mencegah seseorang untuk memanipulasi internal secara langsung, seperti a._re = 7.
Cara termudah untuk membuat kelas ini benar-benar tidak dapat diubah adalah dengan mewarisi dari basis yang tidak dapat diubah. Dengan asumsi Anda menggunakan setidaknya 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 ...
Kelas NamedTupledasar secara otomatis membuat konstruktor untuk Anda, sehingga Complex(2, 3)menghasilkan 2 + 3inilai kompleks Anda . Jika tidak ada nilai yang diberikan im, seperti Complex(2), default dari 0digunakan untuk im.
Jika Anda ingin mengubah nilai reatau im, Anda harus membuat objek baru.
a = Complex(-8, a.im)
atau, menggunakan NamedTuple._replace:
a = a._replace(re=-8)
Tidak Diterapkan
Pembaca yang cerdik akan memperhatikan hal return NotImplementeddi atas. Ini adalah singleton ajaib, yang merupakan sinyal bagi Python untuk mencoba alternatif. Misalnya, a == bbisa fallback pada not a.__neq__(b), b.__eq__(a)atau bahkan not b.__ne__(a).
Pertimbangkan: Anda mungkin tidak tahu tentang sebuah Matrixkelas, tetapi mungkin tahu tentang Complexkelas Anda . Jika seseorang melakukannya cmplx * matrix, jika __mul__fungsi Anda meningkat TypeError, permainan berakhir. Jika malah NotImplementeddikembalikan, maka Matrix.__rmul__bisa dicoba, yang mungkin berhasil.
Lihat NotImplemented dan Menerapkan operasi aritmatika
rtruediv
Saat mengevaluasi a / b, pertama dicoba a.__truediv__(b). Jika gagal (tidak ditentukan atau dikembalikan NotImplemented), b.__rtruediv__(a)dapat dicoba.
class Complex:
...
def __rtruediv__(self, other):
if isinstance(other, Complex):
...
...
Mengapa isinstance(other, Complex)bisa benar? Ini berarti keduanya selfadalah a Complex(karena kita masuk Complex.__rtruediv__), dan othermerupakan Complex(karena isinstancedikatakan demikian dalam skenario ini). Tetapi jika itu masalahnya, yang kami lakukan Complex() / Complex(), maka __truediv__seharusnya sudah digunakan, dan __rtruediv__bahkan tidak perlu dipertimbangkan.
Divisi oleh Nol
Mengapa Complex(2, 2) / 0menaikkan ZeroDivisionErrortempat sebagai Complex(2, 2) / Complex(0, 0)kenaikan ValueError? Bukankah seharusnya itu meningkatkan a ZeroDivisionError?
Nama pengujian Anda test_complex_division_raises_zero_division_exceptiontidak cocok dengan with self.assertRaises(ValueError)kondisi, yang menunjukkan Anda tahu apa yang seharusnya dimunculkan, dan menemukan kesalahan, tetapi mengubah pengujian agar cocok dengan kondisi yang dimunculkan, alih-alih memunculkan pengecualian yang benar.