Python學習之路29-序列的修改、散列和切片

《流暢的Python》筆記。

本篇是「面向對象慣用方法」的第三篇。本篇將以上一篇中的Vector2d爲基礎,定義多維向量Vector。python

1. 前言

自定義Vector類的行爲將與Python標準中的不可變扁平序列同樣,它將支持以下功能:編程

  • 基本的序列協議:__len____getitem__
  • 正確表述擁有不少元素的實例;
  • 適當的切片支持,用於生成新的Vector實例;
  • 綜合各個元素的值計算散列值;
  • 自定義的格式語言擴展。

本篇還將經過__getattr__方法實現屬性的動態存取(雖然序列類型一般不會這麼作),以及穿插討論一個概念:把協議當作正式接口。咱們將說明協議和鴨子類型之間的關係,以及對自定義類型的影響。bash

2. 第一版Vector

Vector的構造方法將和全部內置序列類型同樣,以可迭代對象爲參數。若是其中元素過多,repr()函數返回的字符串將會使用...省略一部份內容,它的初始版本以下:微信

# 代碼1
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):   # 和Vector2d相同
        return str(tuple(self))

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

    def __eq__(self, other):   # 和Vector2d相同
        return tuple(self) == tuple(other)

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

    def __bool__(self):   # 和Vector2d相同
        return bool(abs(self))

    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)   # 去掉了Vector2d中的星號*

之因此沒有直接繼承製Vector2d,既是由於這兩個類的構造方法不兼容,也是由於咱們要爲Vector實現序列協議。ssh

3. 協議和鴨子類型

協議和鴨子類型在以前的文章中也有所說起。在面向對象編程中,協議是非正式的接口,只在文檔中定義,在代碼中不定義。函數

在Python中,只要實現了協議須要的某些方法,其實就算實現了協議,而不必定須要繼承。好比只要實現了__len____getitem__這兩個方法,那麼這個類就是知足序列協議的,而不須要從什麼「序列基類」繼承。測試

鴨子類型:和現實中相反,Python中肯定一個東西是否是「鴨子」,不是測它的「DNA」是否是」鴨子「的DNA,而是看這東西像不像只鴨子。只要像」鴨子「,那它就是「鴨子」。好比,只要一個類實現了__len____getitem__方法,那它就是序列類,而沒必要管它是從哪來的;文件類對象也常是鴨子類型。網站

4. 第2版Vector:支持切片

Vector變爲序列類型,並能正確返回切片:spa

# 代碼2,將如下代碼添加到第一版Vector中
class Vector:
    -- snip --
    def __len__(self):
        return len(self._components)
    
    def __getitem__(self, index):
        cls = type(self)
        if isinstance(index, slice):  # 若是index是個切片類型,則構造新實例
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral):  # 若是index是個數,則直接返回
            return self._components[index]
        else:
            msg = "{cls.__name__} indices must be integers"
            raise TypeError(msg.format(cls=cls))

若是__getitem__函數直接返回切片:return self._components[index],那麼獲得的數據將是array類型,而不是Vector類型。正是爲了使切片的類型正確,這裏才作了類型判斷。.net

上述代碼中用到了slice類型,它是Python的內置類型,這裏順便補充一下切片原理,直接上代碼:

# 代碼3
>>> class MySeq:
...     def __getitem__(self, index):
...         return index  # 直接返回傳給它的值
...    
>>> s = MySeq()
>>> s[1]   
1  # 單索引,沒啥新奇的
>>> s[1:3]
slice(1, 3, None)  # 返回來一個slice類型
>>> s[1:10:2]
slice(1, 10, 2)    # 注意slice類型的結構
>>> s[1:10:2, 9]
(slice(1, 10, 2), 9)   # 若是[]中有逗號,__getitem__收到的是元組
>>> s[1:10:2, 7:9]
(slice(1, 10, 2), slice(7, 9, None))

>>> dir(slice)  # 注意最後四個元素
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', 
'__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__', 'indices', 'start', 'step', 'stop']

當咱們用dir()函數獲取slice的屬性時,發現它有startstopstep數據屬性,而且還有一個indices方法,這裏重點說說這個indices方法。它接收一個長度參數len,並根據這個lenslice類型的startstopstep三個參數正確轉換成在長度範圍內的非負數,具體用法以下:

# 代碼4
>>> slice(None, 10, 2).indices(5)
(0, 5, 2)  # 將這些煩人的索引通通轉換成明確的正向索引
>>> slice(-3, None, None).indices(5)
(2, 5, 1)

自定義Vector類中並無使用這個方法,由於Vector的底層咱們使用了array.array數據類型,切片的具體操做不用咱們自行編寫。但若是你的類沒有這樣的底層序列類型作支撐,那麼slice.indices方法將爲你節省大量時間。

5. 第3版Vector:動態存儲屬性

目前版本的Vector中,沒有辦法經過名稱訪問向量的份量(如v.xv.y),並且如今的Vector可能存在大量份量。不過,若是能經過單個字母訪問前幾個份量的話,這樣將很方便,也更人性化。如今,咱們想用xyzt四個字母分別代替v[0]v[1]v[2]v[3],但具體作法並非爲實例添加這四個屬性,而且咱們也不想在運行時實例能動態添加單個字母的屬性,更不想實例能經過這四個字母修改Vectorself._components的值。換句話說,咱們只想經過這四個字母提供一種較爲方便的訪問方式,僅此而已。而要實現這樣的功能,則須要實現__getattr____setattr__方法,如下是它們的代碼:

# 代碼5.1
class Vector:
    -- snip --
    
    shortcut_name = "xyzt"

    def __getattr__(self, name):
        cls = type(self)
        if len(name) == 1:   # 若是屬性是單個字母
            pos = cls.shortcut_name.find(name)    
            if 0 <= pos < len(self._components):  # 判斷是否是xyzt中的一個
                return self._components[pos]   
        msg = "{.__name__!r} object has no attribute {!r}"  # 想要獲取其餘屬性時則拋出異常
        raise AttributeError(msg.format(cls, name))

    def __setattr__(self, name, value):
        cls = type(self)
        if len(name) == 1:   # 不容許建立單字母實例屬性,即使是x,y,z,t
            if name in cls.shortcut_name:  # 若是name是xyzt中的一個,設置特殊的錯誤信息
                error = "readonly attibute {attr_name!r}"
            elif name.islower():  # 爲小寫字母設置特殊的錯誤信息
                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)

解釋:

  • 屬性查找失敗後,解釋器會調用__getattr__方法。簡單來講,對my_obj.x表達式,Python會檢查my_obj實例有沒有名爲x實例屬性;若是沒有,則到它所屬的類中查找有沒有名爲x類屬性;若是仍是沒有,則順着繼承樹繼續查找。若是依然找不到,則會調用my_obj所屬類中定義的__getattr__方法,傳入self和屬性名的字符串形式(如'x');
  • __getattr____setattr_方法通常同時定義,不然對象的行爲很容易出現不一致。好比,若是這裏定義__getattr__方法,則會出現以下尷尬的代碼:

    # 代碼5.2
    >>> v = Vector(range(5))
    >>> v
    Vector([0.0, 1.0, 2.0, 3.0, 4.0])
    >>> v.x
    0.0
    >>> v.x = 10  # 按理說這裏應該報錯纔對,由於不容許修改
    >>> v.x
    10
    >>> v  # 實際上是v建立了新實例屬性x,這也是爲何咱們要定義__setattr__
    Vector([0.0, 1.0, 2.0, 3.0, 4.0])   # 行爲不一致
  • 咱們沒有禁止動態添加屬性,只是禁止爲單個字母屬性賦值,若是屬性名的長度大於1,這樣的屬性是能夠動態添加的;
  • 若是你看過上一篇文章,那麼你可能會想到用__slots__來禁止添加屬性,但咱們這裏仍然選擇實現__setattr__來實現此功能。__slots__屬性最好只用於節省內存,並且僅在內存嚴重不足時才用它,別爲了秀操做而寫一些別人看着很彆扭的代碼(只寫給本身看的除外)。

6. 第4版Vector:散列和快速等值測試

目前這個Vector是不可散列的,如今咱們來實現__hash__方法。具體方法和上一篇同樣,也是用各個份量的哈希值進行異或運算,因爲Vector的份量可能不少,這裏咱們使用functools.reduce函數來歸約異或值。同時,咱們還將改寫以前那個簡潔版的__eq__,使其更高效(至少對大型向量來講更高效):

# 代碼6,請自行導入所需的模塊
class Vector:
    -- snip --
    def __hash__(self):
        hashs = (hash(x) for x in self._components)   # 先求各個份量的哈希值
        return functools.reduce(operator.xor, hashs, 0)  # 而後將全部哈希值歸約成一個值

    def __eq__(self, other): # 不用像以前那樣:生成元組只爲使用元組的__eq__方法
        return len(self) == len(self) and all(a == b for a, b in zip(self, other))

解釋:

  • 此處的__hash__方法實際上執行的是一個映射歸約的過程。每一個份量被映射成了它們的哈希值,這些哈希值再歸約成一個值;
  • 這裏的functool.reduce傳入了第三個參數,而且建議最好傳入第三個參數。傳入第三個參數能避免這個異常:TypeError: reduce() of empty sequence with no initial value。若是序列爲空,第三個參數就是返回值;不然,在歸約中它將做爲第一個參數;
  • __eq__方法中先比較兩序列的長度並不只僅是一種捷徑。zip函數並行遍歷多個可迭代對象,若是其中一個耗盡,它會當即中止生成值,並且不發出警告;
補充一個小知識: zip函數和文件壓縮沒有關係,它的名字取自拉鍊頭(zipper fastener),這個小物件把兩個拉鍊條的鏈牙要合在一塊兒,是否是很形象?

7. 第5版Vector:格式化

Vector2d中,當傳入'p'時,以極座標的形式格式化數據;因爲Vector的維度可能大於2,如今,當傳入參數'h'時,咱們使用球面座標格式化數據,即'<r, Φ1, Φ2, Φ3>'。同時,還須要定義兩個輔助方法:

  • angle(n),用於計算某個角座標;
  • angles(),返回由全部角座標構成的可迭代對象。

至於這兩個的數學原理就不解釋了。如下是最後要添加的代碼:

# 代碼7
class Vector:
    -- snip --
    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
        return a

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

    def __format__(self, format_spec=""):
        if format_spec.endswith("h"):   # 若是格式說明符以'h'結尾
            format_spec = format_spec[:-1]   # 格式說明符前面部分保持不變
            coords = itertools.chain([abs(self)], self.angles())  # 
            outer_fmt = "<{}>"
        else:
            coords = self
            outer_fmt = "({})"
        components = (format(c, format_spec) for c in coords)
        return outer_fmt.format(", ".join(components))

itertools.chain函數生成生成器表達式,將多個可迭代對象鏈接成在一塊兒進行迭代。關於生成器的更多內容將在之後的文章中介紹。

至此,多維Vector暫時告一段落。


迎你們關注個人微信公衆號"代碼港" & 我的網站 www.vpointer.net ~

相關文章
相關標籤/搜索