建立一種新的資料型別
我們都知道在 Python 中有各式和各樣的資料型別, 比如 int, float, 在語言的定義下, 我們很容易作一些動作, 比如加減乘除, 但是一旦我們有新的資料型別, 這些動作我們就用不了, 還要定義各種函式來使用.
現在本文就是要教你怎麼定義一個新的資料型別, 而且還要能使用 Python 的語法來作一些基本的動作, 這裡以二維座標點為例, 我們都知道一個座標點是 P(x, y), x, y 為整數或浮點數, 原則上就是一個長度為 2 的簡單 tuple. 如果我們要作一些運算, 就得呼叫函式, 那用起來就很麻煩.
所以, 我們定義了一個新的資料型別來處理, 以下就是其作法, 類似的方式可運用到其他的地方.
新資料型別的類定義
- 因為我們的座標基本上就是一個 tuple, 所以我們以 tuple 為其父類
class Coordinate(tuple):
- 該類的類呼叫返回是一個 tuple, 而不是一個一般的類例項, 這裡我們就不能呼叫 __init__, 因為它只是作初始化動作, 所以我應該呼叫的是 __new__. 這第一個引數不是 self, 因為一開始該例項並未存在, 所以使用的是 cls (class), 呼叫 tuple.__new__ 來建立一個 tuple, 並返回該 tuple, 所以呼叫該類的結果得到的是一個 tuple 值, 但是其類別為 Coordinate.
def __new__(cls, x, y):
return tuple.__new__(cls, (x, y))
- 我們現在要取得該座標的兩個最基本的屬性, x 座標及 y 座標, 怎麼辦呢? 當然可以呼叫 __init__, 不過我們不想需要隨時更新其值再來呼叫, 所以我們用到 functools 的 cached_property 來將函式名變成屬性名, 這樣我們就可以直接呼叫 P.x, P.y, 而不是 P[0], P[1] 或 P.x(), P.y(), 是不是更直觀更簡單了.
這裡也可以使用Python 內建的@property, 這個方式每次都會計算函式結果, 而@cached_property 則會快取結果, 下次壐呼叫就不會重新計算一次. 如果函式的結果只與引數有關, 使用@ cached_property 比較快, 否則還是使用 @property.
from functools import cached_property
@cached_property
def x(self):
return self[0]
@cached_property
def y(self):
return self[1]
- 另外我們希望提供一個屬性, distance, 到原點的距離, 一般我們都使用函式來定義及呼叫, 同樣地, 我們也把它設為屬性, 直接使用 P.distance
import math
@cached_property
def distance(self):
return (self[0]**2 + self[1]**2)**0.5
- 再來我們要作的是提供 python 語法中的加減乘, 這裡沒有除, 因為座標點之間沒有除法. 在 Python 中, 如果運算式為
例項1 運運算元 例項2
, 如果例項2是例項1中運運算元可以處理的, 就會呼叫例項1的該運運算元函式, 如 __add__ (加), __sub__ (減), __mul__ (乘), 所以這裡定義的是以座標點類開始的運算式, 比如 P+1, P1+P2…, 而不是 1+P. 在這我們定義了座標點類與座標點類的運算, 以及座標點類與 int 或 float 的運算. 當然也可以定義與 tuple, list 的運算, 只是麻煩一點, 因為還要去檢查其類別啊, 長度等等, 所以我們就沒這麼定義, 當然也就不允許這樣的運算式.
def __add__(self, other):
if isinstance(other, Coordinate):
return Coordinate(self.x+other.x, self.y+other.y)
elif isinstance(other, (int, float)):
return Coordinate(self.x+other, self.y+other)
else:
raise ValueError
def __sub__(self, other):
if isinstance(other, Coordinate):
return Coordinate(self.x-other.x, self.y-other.y)
elif isinstance(other, (int, float)):
return Coordinate(self.x-other, self.y-other)
else:
raise ValueError
def __mul__(self, other):
if isinstance(other, Coordinate):
return Coordinate(self.x*other.x, self.y*other.y)
elif isinstance(other, (int, float)):
return Coordinate(self.x*other, self.y*other)
else:
raise ValueError
- 定義了左運運算元, 再來就定義一下右運運算元, 所使用的函式名為__radd__ ( 加 ), __rsub__ ( 減 ), __rmul__ ( 乘 ), 這裡因為要使用負數, 所以我們也定義了 +P ( __pos__ 正數 ), -P ( __neg__ 負數 )
def __neg__(self):
return Coordinate(-self.x, -self.y)
def __pos__(self):
return self
def __radd__(self, other):
return self.__add__(other)
def __rsub__(self, other):
return -self.__sub__(other)
def __rmul__(self, other):
return self.__mul__(other)
- 再來我們定義一下, 輸出的顯示格式, 當然不用定義也可以, 因為它是 tuple 的子類, 所以沒定義時它會呼叫 tuple 定義的輸出的顯示格式
def __repr__(self):
return f'({self.x}, {self.y})'
- 如果就只有這樣, 那肯定是不夠的, 要定義一個類, 總會有一些特殊的函式供呼叫, 比如逆時針旋轉某個角度. 這個演算法可以使用幾何數學公式就可以了, 當然這是以原點為參考點.
def rotate(self, angle):
angle = angle * math.pi / 180.0
cos, sin = math.cos(angle), math.sin(angle)
return Coordinate(self.x*cos-self.y*sin, self.x*sin+self.y*cos)
如果要求兩點間的距離, 當然可以另外定義一個函式來計算, 也可以使用 (P1-P2).distance.
在這裡你要小心一件事,例如以下的情況,將會造成加法遞迴不停而出錯。
def __add__(self, other):
return self + other
使用方式
>>> p = Coordinate(3, 4) # 座標
>>> p.x, p.y, p.distance
3, 4, 5.0
>>> p+1, p-1, 1+p, 1-p, 2*p, p*2
(4, 5), (2, 3), (4, 5), (-2, -3), (6, 8), (6, 8)
>>> +p, -p
(3, 4), (-3, -4)
>>> p1, p2 = Coordinate(3, 4), Coordinate(5, 6)
>>> p1+p2, p1-p2, p1*p2
(8, 10), (-2, -2), (15, 24)
>>> p1.rotate(45)
(-0.7071067811865475, 4.949747468305834)
全部原始碼
import math
from functools import cached_property
class Coordinate(tuple):
def __new__(cls, x, y):
return tuple.__new__(cls, (x, y))
@cached_property
def x(self):
return self[0]
@cached_property
def y(self):
return self[1]
@cached_property
def distance(self):
return math.dist(self, (0, 0))
def __add__(self, other):
if isinstance(other, Coordinate):
return Coordinate(self.x+other.x, self.y+other.y)
elif isinstance(other, (int, float)):
return Coordinate(self.x+other, self.y+other)
else:
raise ValueError
def __sub__(self, other):
if isinstance(other, Coordinate):
return Coordinate(self.x-other.x, self.y-other.y)
elif isinstance(other, (int, float)):
return Coordinate(self.x-other, self.y-other)
else:
raise ValueError
def __mul__(self, other):
if isinstance(other, Coordinate):
return Coordinate(self.x*other.x, self.y*other.y)
elif isinstance(other, (int, float)):
return Coordinate(self.x*other, self.y*other)
else:
raise ValueError
def __pos__(self):
return self
def __neg__(self):
return Coordinate(-self.x, -self.y)
def __radd__(self, other):
return self.__add__(other)
def __rsub__(self, other):
return -self.__sub__(other)
def __rmul__(self, other):
return self.__mul__(other)
def __repr__(self):
return f'({self.x}, {self.y})'
def rotate(self, angle):
angle = angle * math.pi / 180.0
cos, sin = math.cos(angle), math.sin(angle)
return Coordinate(self.x*cos-self.y*sin, self.x*sin+self.y*cos)
本作品採用《CC 協議》,轉載必須註明作者和本文連結