第一章(提煉) Python數據模型

        Python風格的關鍵完全體現在Python的數據模型上,而數據模型所描述的API,爲使用最地道的語言特性來構建開發者自己的對象提供了工具。當Python解析器遇到特殊句法時,會使用特殊方法去激活一些基本的對象操作。特殊方法以雙下劃線開頭,以雙下劃線結尾(如:__getitem__)。如:obj[key]的背後就是__getitem__方法。魔術方法是特殊方法的暱稱,特殊方法也叫雙下方法。

一. 一摞Python風格的紙牌

使用__getitem__和__len__創建一摞有序的紙牌(使用collections.namedtuple構建了一個簡單的類來表示一張紙牌,自python2.6開始,namedtuple就加入到python裏,用以構建只有少數屬性但沒有方法的類):

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])


class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    # ♠, ♡, ♣, ♢,
    suits = ['\u2660', '\u2661', '\u2663', '\u2662']

    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks]

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

    def __getitem__(self, item):
        return self._cards[item]


# 撲克牌排序:2最小,A最大,花色由大到小:♠, ♡, ♢,♣
suit_values = {'\u2660': 3, '\u2661': 2, '\u2662': 1, '\u2663': 0}


def sort_rule(card):
    rank_value = FrenchDeck.ranks.index(card.rank)
    return rank_value * len(suit_values) + suit_values[card.suit]

演示1  我們自定義的FrenchDeck類可以像任何python標準集合類型一樣,可以使用len()函數;查看一疊牌有多少張:

演示2  d[0]或d[-1]由__getitem__方法提供:

演示3  random.choice方法用於從一個序列中隨機選出一個元素;隨機取出一張紙牌:

想必,現在你已經體會到通過python特殊方法來利用Python數據模型的2個好處:

  1. 作爲類的用戶,無需去記住標準操作的各種名詞,如獲取長度是.size,還是.length,還是別的什麼?
  2. 可以更加方便地利用python的標準庫,如random.choice函數。

演示4  因爲__getitem__方法把[]操作交給了self.cards列表,所有我們的FrenchDeck實例自動支持切片:

演示5  僅僅實現了__getitem__方法,這一摞牌即可迭代:

反向迭代也OK:

演示6  迭代通常是隱式的,比如一個集合類型沒有實現__contains__方法,那麼in運算符就會按順序做一次迭代搜索。因此,in運算符可以用在我們的FrenchDeck實例上,因爲它是可迭代的:

演示7 使用python標準庫中的sorted函數:

總結:雖然FrenchDeck隱式地繼承了object類,但功能卻不是繼承而來的。通過實現__len__和__getitem__這兩個特殊方法,FrenchDeck類就跟一個Python自有的序列數據類型一樣,可以體現出Python的核心語言特性(例如迭代和切片)。同時這個類還可用於標準庫中諸如random.choice、reversed和sorted這些函數。

演示8  按照目前的設計,FrenchDeck還不支持洗牌,因爲它是不可變的:

錯誤消息相當明確:對象不支持元素賦值。原因是,shuffle函數要調換集合中元素的位置,而FrenchDeck只實現了不可變的序列協議。可變的序列還必須提供__setitem__方法。

演示9  爲FrenchDeck打猴子補丁,把它變成可變的,讓random.shuffle函數能處理:

備註:Python是動態語言,因此我們可以在運行時添加__setitem__方法,甚至是在交互式控制檯中添加。演示9中把set_card函數賦值給特殊方法__setitem__,從而把它依附到FrenchDeck類上。這種技術叫猴子補丁 :在運行時修改類或模塊,而不改動源碼。猴子補丁很強大,但猴子補丁的代碼與要打補丁的程序耦合十分緊密。

        定義set_card函數時使用的參數爲deck,position,card;而在類中創建__setitem__方法默認使用的參數是self,key和value。此處這樣定義,只是爲了說明每個Python方法說到底都是普通函數,把第一個參數命名爲self只是一種約定。在控制檯會話中使用那幾個參數沒問題,不過在Python源碼文件中最好按照文檔那樣使用self、key和value。

二. 如何使用特殊方法

        首先明確一點,特殊方法的存在是爲了被Python解析器調用的。即沒有obj.__len__()這種寫法,而是len(obj)。在執行len(obj)時,如果obj是一個自定義類的對象,那麼python會自己去調用我們實現的__len__方法。

        然而如果是python內置類型,比如列表、字符串。字節序列等,那麼CPython會抄個近路,__len__實際上會返回PyVarObject裏的ob_size屬性;直接讀取這個值比調用一個方法要快得多。很多時候,特殊方法的調用是隱式的,比如for i in x:這個語句其實是調用iter(x),而這個函數的背後是x.__iter__()方法。

        通過內置函數(如:len、iter、str等等)來使用特殊方法是最好的選擇。這些內置函數不僅會調用這些方法,通常還提供額外的好處,而且對於內置類型來說,它們的速度更快。

2.1 模擬數值類型

一個簡單的二維向量類

from math import hypot


class Vector:
    """自定義二維向量"""
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Vector({self.x},{self.y})'

    def __abs__(self):
        return hypot(self.x, self.y)

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

    def __add__(self, other):
        x = self.x + other.x
        y = self.y + other.y
        return Vector(x, y)

    def __mul__(self, scaler):
        return Vector(self.x * scaler, self.y * scaler)

演示1  自定義二維向量的使用

備註:python內置的repr函數能把一個對象用字符串的形式表達出來以便辨認,這就是字符串表示形式。repr是通過調用__repr__這個特殊方法來得到一個對象的字符串表示形式的。交互式控制檯和調試程序用repr函數來獲取字符串表示形式。__repr__和__str__的區別在於,後者是在str()函數被調用或是在用print函數打印一個對象的時候才調用的,並且它返回的字符串對終端用戶更友好。如果你只想實現這2個特殊方法中的一個,__repr__是更好的選擇,因爲如果一個對象沒有__str__函數,而python又需要調用它的時候,解析器會調用__repr__作爲替代。

        通過__add__和__mul__爲向量類帶來了+和*兩個運算符。此外還可以使用__radd__和__rmul__實現交換律。

        默認情況下,我們自己定義的類的實例總被認爲是真的,除非這個類對__bool__或者__len__函數有自己的實現。bool(x)的背後是調用x.__bool__()的結果;如果不存在__bool__方法,那麼bool(x)會調用x.__len__()。若返回0,則bool會返回False;否則返回True。

2.2 特殊方法一覽

跟運算符無關的特殊方法
類別 方法名
字符串/字節序列表示形式 __repr__、__str__、__format__、__bytes__
數值轉換 __abs__、__bool__、__complex__、__int__、__float__、__hash__、__index__
集合模擬 __len__、__getitem__、__setitem__、__delitem__、__contains__
迭代枚舉 __iter__、__reversed__、__next__
可調用模式 __call__
上下文管理 __enter__、__exit__
實例的創建和銷燬 __nex__、__init__、__del__
屬性管理 __getattr__、__getattribute__、__setattr__、__delattr__、__dir__
屬性描述符 __get__、__set__、__delete__
跟類相關的服務 __prepare__、___instancecheck__、__subclasscheck__
跟運算符相關的特殊方法
類別 方法名和對應的運算符
一元運算符 __neg__ -、__pos__ +、__abs__ abs()
比較運算符 __lt__ <、__le__ <=、__eq__ ==、__ne__ !=、__gt__ >、__ge__ >=
算數運算符 __add__ +、__sub__ -、__mul__ *、__truediv__ /、__floordiv__ //、__mod__ %、__divmod__ divmode()、__pow__ **/pow()、__round__ round()
反向算數運算符 __radd__、__rsub__、__rmul__、__rtruediv__、__rfloordiv__、__rmod__、__rdivmod__、
增量賦值算術運算符 __iadd__、__isub__、__imul__、__itruediv__、__ifloordiv、__imod__、__ipow__、、、
位運算符 __invert__ ~、__lshift__ <<、__rshift__ >>、__and__ &、__or__ |、__xor__ ^
反向位運算符 __rlshift__、__rrshift__、__rand__、__ror__、__rxor__
增量賦值位運算符 __ilshift__、__irshift__、__iand__、__ior__、__ixor__
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章