Python如何設計物件導向的類(下)

測試開發客棧發表於2021-07-16

本文將在上篇文章二維向量Vector2d類的基礎上,定義表示多維向量的Vector類。

第1版:相容Vector2d類

程式碼如下:

from array import array
import reprlib
import math


class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)  # 多維向量存陣列中

    def __iter__(self):
        return iter(self._components)  # 構建迭代器

    def __repr__(self):
        components = reprlib.repr(self._components)  # 有限長度表示形式
        components = components[components.find('['):-1]
        return 'Vector({})'.format(components)

    def __str__(self):
        return str(tuple(self))

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) +
                bytes(self._components))

    def __eq__(self, other):
        return tuple(self) == tuple(other)

    def __abs__(self):
        return math.sqrt(sum(x * x for x in self))

    def __bool__(self):
        return bool(abs(self))

    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)  # 因為建構函式入參是陣列,所以不用再使用*拆包了

其中的reprlib.repr()函式用於生成大型結構或遞迴結構的安全表達形式,比如:

>>> Vector([3.1, 4.2])
Vector([3.1, 4.2])
>>> Vector((3, 4, 5))
Vector([3.0, 4.0, 5.0])
>>> Vector(range(10))
Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])

超過6個的元素用...來表示。

第2版:支援切片

Python協議是非正式的介面,只在文件中定義,在程式碼中不定義。比如Python的序列協議只需要__len____getitem__兩個方法,Python的迭代協議只需要__getitem__一個方法,它們不是正式的介面,只是Python程式設計師預設的約定。

切片是序列才有的操作,所以Vector類要實現序列協議,也就是__len____getitem__兩個方法,程式碼如下:

def __len__(self):
    return len(self._components)

def __getitem__(self, index):
    cls = type(self)  # 獲取例項所屬的類
    if isinstance(index, slice):  # 如果index是slice切片物件
        return cls(self._components[index])  # 呼叫構造方法,返回新的Vector例項
    elif isinstance(index, numbers.Integral):  # 如果index是整型
        return self._components[index]  # 直接返回元素
    else:
        msg = '{cls.__name__} indices must be integers'
        raise TypeError(msg.format(cls=cls))

測試一下:

>>> v7 = Vector(range(7))
>>> v7[-1]  # <1>
6.0
>>> v7[1:4]  # <2>
Vector([1.0, 2.0, 3.0])
>>> v7[-1:]  # <3>
Vector([6.0])
>>> v7[1,2]  # <4>
Traceback (most recent call last):
  ...
TypeError: Vector indices must be integers

第3版:動態存取屬性

通過實現__getattr____setattr__,我們可以對Vector類動態存取屬性。這樣就能支援v.my_property = 1.1這樣的賦值。

如果使用__setitem__方法,那麼只能支援v[0] = 1.1

程式碼如下:

shortcut_names = 'xyzt'  # 4個分量屬性名

def __getattr__(self, name):
    cls = type(self)  # 獲取例項所屬的類
    if len(name) == 1:  # 只有一個字母
        pos = cls.shortcut_names.find(name)
        if 0 <= pos < len(self._components):  # 落在範圍內
            return self._components[pos]
    msg = '{.__name__!r} object has no attribute {!r}'  # <5>
    raise AttributeError(msg.format(cls, name))


def __setattr__(self, name, value):
    cls = type(self)
    if len(name) == 1:  
        if name in cls.shortcut_names:  # name是xyzt其中一個不能賦值
            error = 'readonly attribute {attr_name!r}'
        elif name.islower():  # 小寫字母不能賦值,防止與xyzt混淆
            error = "can't set attributes 'a' to 'z' in {cls_name!r}"
        else:
            error = ''
        if error:
            msg = error.format(cls_name=cls.__name__, attr_name=name)
            raise AttributeError(msg)
    super().__setattr__(name, value)  # 其他name可以賦值

值得說明的是,__getattr__的機制是:對my_obj.x表示式,Python會檢查my_obj例項有沒有名為x的屬性,如果有就直接返回,不呼叫__getattr__方法;如果沒有,到my_obj.__class__中查詢,如果還沒有,才呼叫__getattr__方法

正因如此,name是xyzt其中一個時才不能賦值,否則會出現下面的奇怪現象:

>>> v = Vector([range(5)])
>>> v.x = 10
>>> v.x
10
>>> v
Vector([0.0, 1.0, 2.0, 3.0, 4.0])

對v.x進行了賦值,但實際未生效,因為賦值後Vector新增了一個x屬性,值為10,對v.x表示式來說,直接就返回了這個值,不會走我們自定義的__getattr__方法,也就沒辦法拿到v[0]的值。

第4版:雜湊

通過實現__hash__方法,加上現有的__eq__方法,Vector例項就變成了可雜湊的物件。

程式碼如下:

import functools
import operator


def __eq__(self, other):
    return (len(self) == len(other) and
            all(a == b for a, b in zip(self, other)))

def __hash__(self):
    hashes = (hash(x) for x in self)  # 建立一個生成器表示式
    return functools.reduce(operator.xor, hashes, 0)  # 計算聚合的雜湊值

其中__eq__方法做了下修改,用到了歸約函式all(),比tuple(self) == tuple(other)的寫法,能減少處理時間和記憶體。

zip()函式取名自zipper拉鍊,把兩個序列咬合在一起。比如:

>>> list(zip(range(3), 'ABC'))
[(0, 'A'), (1, 'B'), (2, 'C')]

第5版:格式化

Vector的格式化跟Vector2d大同小異,都是定義__format__方法,只是計算方式從極座標換成了球面座標:

def angle(self, n):
    r = math.sqrt(sum(x * x for x in self[n:]))
    a = math.atan2(r, self[n-1])
    if (n == len(self) - 1) and (self[-1] < 0):
        return math.pi * 2 - a
    else:
        return a

def angles(self):
    return (self.angle(n) for n in range(1, len(self)))

def __format__(self, fmt_spec=''):
    if fmt_spec.endswith('h'):  # hyperspherical coordinates
        fmt_spec = fmt_spec[:-1]
        coords = itertools.chain([abs(self)],
                                 self.angles())
        outer_fmt = '<{}>'
    else:
        coords = self
        outer_fmt = '({})'
    components = (format(c, fmt_spec) for c in coords)
    return outer_fmt.format(', '.join(components))

極座標和球面座標是啥?我也不知道,略過就好。

小結

經過上下兩篇文章的介紹,我們知道了Python風格的類是什麼樣子的,跟常規的物件導向設計不同的是,Python的類通過魔法方法實現了Python協議,使Python類在使用時能夠享受到語法糖,不用通過get和set的方式來編寫程式碼

參考資料:

《流暢的Python》第10章 序列的修改、雜湊和切片

相關文章