建立一種新的資料型別

Jason990420發表於2020-06-07

建立一種新的資料型別

我們都知道在 Python 中有各式和各樣的資料型別, 比如 int, float, 在語言的定義下, 我們很容易作一些動作, 比如加減乘除, 但是一旦我們有新的資料型別, 這些動作我們就用不了, 還要定義各種函式來使用.

現在本文就是要教你怎麼定義一個新的資料型別, 而且還要能使用 Python 的語法來作一些基本的動作, 這裡以二維座標點為例, 我們都知道一個座標點是 P(x, y), x, y 為整數或浮點數, 原則上就是一個長度為 2 的簡單 tuple. 如果我們要作一些運算, 就得呼叫函式, 那用起來就很麻煩.

所以, 我們定義了一個新的資料型別來處理, 以下就是其作法, 類似的方式可運用到其他的地方.

新資料型別的類定義

  1. 因為我們的座標基本上就是一個 tuple, 所以我們以 tuple 為其父類
class Coordinate(tuple):
  1. 該類的類呼叫返回是一個 tuple, 而不是一個一般的類例項, 這裡我們就不能呼叫 __init__, 因為它只是作初始化動作, 所以我應該呼叫的是 __new__. 這第一個引數不是 self, 因為一開始該例項並未存在, 所以使用的是 cls (class), 呼叫 tuple.__new__ 來建立一個 tuple, 並返回該 tuple, 所以呼叫該類的結果得到的是一個 tuple 值, 但是其類別為 Coordinate.
    def __new__(cls, x, y):
        return tuple.__new__(cls, (x, y))
  1. 我們現在要取得該座標的兩個最基本的屬性, 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]
  1. 另外我們希望提供一個屬性, distance, 到原點的距離, 一般我們都使用函式來定義及呼叫, 同樣地, 我們也把它設為屬性, 直接使用 P.distance
import math

    @cached_property
    def distance(self):
        return (self[0]**2 + self[1]**2)**0.5
  1. 再來我們要作的是提供 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
  1. 定義了左運運算元, 再來就定義一下右運運算元, 所使用的函式名為__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)
  1. 再來我們定義一下, 輸出的顯示格式, 當然不用定義也可以, 因為它是 tuple 的子類, 所以沒定義時它會呼叫 tuple 定義的輸出的顯示格式
    def __repr__(self):
        return f'({self.x}, {self.y})'
  1. 如果就只有這樣, 那肯定是不夠的, 要定義一個類, 總會有一些特殊的函式供呼叫, 比如逆時針旋轉某個角度. 這個演算法可以使用幾何數學公式就可以了, 當然這是以原點為參考點.
    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)
  1. 如果要求兩點間的距離, 當然可以另外定義一個函式來計算, 也可以使用 (P1-P2).distance.

  2. 在這裡你要小心一件事,例如以下的情況,將會造成加法遞迴不停而出錯。

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 協議》,轉載必須註明作者和本文連結
Jason Yang

相關文章