創(chuàng)建一個符合Python風(fēng)格的對象(2)

創(chuàng)建一個符合Python風(fēng)格的對象(1)中伪很,定義了一個二維向量 Vector2d 類,現(xiàn)在以該類為基礎(chǔ)杂数,繼續(xù)擴展宛畦,定義表示多維向量的Vector類。
支持的功能如下:

  • 基本的序列協(xié)議揍移,__len____getitem__
  • 正確表述擁有很多元素的實例
  • 適當(dāng)?shù)那衅С执魏停糜谏a(chǎn)新的Vector實例
  • 綜合各個元素的值計算散列值
  • 自定義的格式語言擴展

此外,通過 __getattr__ 方法實現(xiàn)屬性的動態(tài)存取那伐,以此取代 Vector2d 使用的只讀特性——不過踏施,序列類型通常不會這么做石蔗。

下面來一步步實現(xiàn)。

1.為了支持N維向量畅形,讓構(gòu)造函數(shù)接受可迭代對象

    def __init__(self, components):
        # 把 Vector 的分量保存在一個數(shù)組中
        self._components = array(self.typecode, components)

2.為了支持迭代养距,使用self.components構(gòu)建一個迭代器

    def __iter__(self):
        return iter(self._components)

3.使用reprlib.repr() 函數(shù)獲取 self._components 的有限長度表示形式(如 array('d', [0.0, 1.0, 2.0, 3.0, 4.0, ...])

    def __repr__(self):
        components = reprlib.repr(self._components)
        # 去掉前面的 array('d' 和后面的 )。
        components = components[components.find('['):-1]
        return 'Vector({})'.format(components)

4.直接使用self.components構(gòu)建bytes對象

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

5計算模

    def __abs__(self):
        """計算各分量的平方之和日熬,然后再使用 sqrt 方法開平方"""
        return math.sqrt(sum(x * x for x in self))

6.針對frombytes棍厌,直接把 memoryview 傳給構(gòu)造方法,不用像前面那樣使用 * 拆包

@classmethod
def frombytes(cls, octets):
    typecode = chr(octets[0])
    memv = memoryview(octets[1:]).cast(typecode)
    return cls(memv) 

7.為了支持序列協(xié)議竖席,實現(xiàn)__len____getitem__方法

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

    def __getitem__(self, index):
        """自定義切片操作"""
        cls = type(self)
        #  如果 index 參數(shù)的值是 slice 對象,調(diào)用類的構(gòu)造方法毕荐,使用 _components 數(shù)組的切片構(gòu)建一個新 Vector 實例
        if isinstance(index, slice):
            return cls(self._components[index])
        # 如果 index 是 int 或其他整數(shù)類型,那就返回 _components 中相應(yīng)的元素
        elif isinstance(index, numbers.Integral):
            return self._components[index]
        # 否則憎亚,拋出異常
        else:
            msg = '{.__name__} indices must be integers'
            raise TypeError(msg.format(cls))

8.動態(tài)存取屬性
因為現(xiàn)在是N維向量第美,使用Vector2d中獲取屬性的方式顯然太麻煩。
要想依舊使用my_obj.x方式獲取屬性牲览,可以實現(xiàn)__getattr__方法第献,因為屬性查找失敗后庸毫,解釋器會調(diào)用 __getattr__ 方法飒赃。

    # 定義幾個可以獲取的常用分量
    shortcut__names = 'xyzt'

    def __getattr__(self, name):
        """檢查所查找的屬性是不是 shortcut__names 中的某個字母载佳,如果是臀栈,那么返回對應(yīng)的分量。"""
        cls = type(self)
        # 如果屬性名只有一個字母姑躲,可能是shortcut_names 中的一個
        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}'
        raise AttributeError(msg.format(cls, name))

但是僅僅實現(xiàn)這樣一個方法還不夠,需要注意到對于實例v卖怜,如果執(zhí)行了v.x命令阐枣,實際上v對象就有x屬性了侮繁,因此使用v.x不會調(diào)用__getattr__方法宪哩。
為了避免上述情況锁孟,需要改寫Vector類中設(shè)置屬性的邏輯品抽,通過自定義__setattr__方法實現(xiàn)圆恤。

def __setattr__(self, name, value):
    cls = type(self)
    # 特別處理名稱是單個字符的屬性
    if len(name) == 1:
        # 如果 name 是 shortcut_names 中的一個盆昙,設(shè)置特殊的錯誤消息
        if name in cls.shortcut_names:
            error = 'readonly attribute {attr_name!r}'
        # 如果 name 是小寫字母淡喜,為所有小寫字母設(shè)置一個錯誤消息
        elif name.islower():
            error = "can't set attributes 'a' to 'z' in {cls_name!r}"
        # 否則炼团,把錯誤消息設(shè)為空字符串
        else:
            error = ''
        #  如果有錯誤消息,拋出 AttributeError
        if error:
            msg = error.format(cls_name=cls.__name__, attr_name=name)
            raise AttributeError(msg)
    # 默認(rèn)情況:在超類上調(diào)用 __setattr__ 方法疏尿,提供標(biāo)準(zhǔn)行為
    super().__setattr__(name, value)

在類中聲明 __slots__ 屬性也可以防止設(shè)置新實例屬性。但是不建議只為了避免創(chuàng)建實例屬性而使用 __slots__ 屬性褥琐。__slots__ 屬性只應(yīng)該用于節(jié)省內(nèi)存,而且僅當(dāng)內(nèi)存嚴(yán)重不足時才應(yīng)該這么做踩衩。
另外嚼鹉,為了將該類實例變成是可散列的,需要保持Vector是不可變的锚赤。

9.支持散列和快速等值測試

    def __eq__(self, other):
        # 首先要檢查兩個操作數(shù)的長度是否相同匹舞,因為 zip 函數(shù)會在最短的那個操作數(shù)耗盡時停止,而且不發(fā)出警告线脚。
        # 然后再依次比較兩個序列中的每一個元素
        return (len(self) == len(other) and all(a == b for a, b in zip(self, other)))

    def __hash__(self):
        # 創(chuàng)建一個生成器表達(dá)式,惰性計算各個分量的散列值
        hashes = (hash(x) for x in self)
        # 把 hashes 提供給 reduce 函數(shù)姊舵,使用 xor 函數(shù)計算聚合的散列值寓落;第三個參數(shù),0 是初始值
        return functools.reduce(operator.xor, hashes, 0)

10.格式化
Vector 類支持 N 個維度仰税,所以這里使用球面坐標(biāo),格式后綴定義為'h'。這里的難點主要是涉及數(shù)學(xué)原理河绽,理解意思即可拦赠。具體可以查看n 維球體

def angle(self, n):
    """使用公式計算某個角坐標(biāo)"""
    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):
    """創(chuàng)建生成器表達(dá)式,按需計算所有角坐標(biāo)"""
    return (self.angle(n) for n in range(1, len(self)))

def __format__(self, fmt_spec=''):
    if fmt_spec.endswith('h'):  # 超球面坐標(biāo)
        fmt_spec = fmt_spec[:-1]
        # 使用 itertools.chain 函數(shù)生成生成器表達(dá)式葵姥,無縫迭代向量的模和各個角坐標(biāo)
        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))

下面給出完整代碼

from array import array
import reprlib
import math
import numbers
import functools
import operator
import itertools


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 (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)

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

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

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

    def __getitem__(self, index):
        cls = type(self)
        if isinstance(index, slice):
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral):
            return self._components[index]
        else:
            msg = '{.__name__} indices must be integers'
            raise TypeError(msg.format(cls))

    shortcut_names = 'xyzt'

    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}'
        raise AttributeError(msg.format(cls, name))

    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'):  # 超球面坐標(biāo)
            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))

    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末荷鼠,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子榔幸,更是在濱河造成了極大的恐慌允乐,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件削咆,死亡現(xiàn)場離奇詭異牍疏,居然都是意外死亡,警方通過查閱死者的電腦和手機拨齐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門鳞陨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瞻惋,你說我怎么就攤上這事厦滤≡遥” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵掏导,是天一觀的道長享怀。 經(jīng)常有香客問我,道長趟咆,這世上最難降的妖魔是什么添瓷? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮值纱,結(jié)果婚禮上鳞贷,老公的妹妹穿的比我還像新娘。我一直安慰自己虐唠,他們只是感情好搀愧,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著凿滤,像睡著了一般妈橄。 火紅的嫁衣襯著肌膚如雪庶近。 梳的紋絲不亂的頭發(fā)上翁脆,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天,我揣著相機與錄音鼻种,去河邊找鬼反番。 笑死,一個胖子當(dāng)著我的面吹牛叉钥,可吹牛的內(nèi)容都是我干的罢缸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼投队,長吁一口氣:“原來是場噩夢啊……” “哼枫疆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起敷鸦,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤息楔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扒披,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體值依,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年碟案,在試婚紗的時候發(fā)現(xiàn)自己被綠了愿险。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡价说,死狀恐怖辆亏,靈堂內(nèi)的尸體忽然破棺而出风秤,到底是詐尸還是另有隱情,我是刑警寧澤褒链,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布唁情,位于F島的核電站,受9級特大地震影響甫匹,放射性物質(zhì)發(fā)生泄漏甸鸟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一兵迅、第九天 我趴在偏房一處隱蔽的房頂上張望抢韭。 院中可真熱鬧,春花似錦恍箭、人聲如沸刻恭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鳍贾。三九已至,卻和暖如春交洗,著一層夾襖步出監(jiān)牢的瞬間骑科,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工构拳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留咆爽,地道東北人。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓置森,卻偏偏與公主長得像斗埂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子凫海,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容