python 數據模型

最近開始閱讀《流暢的python》,也會開始更新這本書的學習筆記

第一篇的內容是第一章 python 數據模型,主要是介紹 python 類中的特殊方法(或者說魔術方法),這類特殊方法的實現可以讓我們自定義的類對象能夠使用 python 標準庫的方法,同時也有助於接口方法的一致性。

本文的代碼例子: https://github.com/ccc013/CodesNotes/blob/master/FluentPython/1_Python%E6%95%B0%E6%8D%AE%E6%A8%A1%E5%9E%8B.ipynb


前言

數據模型其實是對 Python 框架的描述,它規範了這門語言自身構建模塊的接口,這些模塊包括但不限於序列、迭代器、函數、類和上下文管理器。

通常在不同框架下寫程序,都需要花時間來實現那些會被框架調用的方法,python 當然也包含這些方法,當 python 解釋器碰到特殊的句法的時候,會使用特殊方法來激活一些基本的對象操作,這種特殊方法,也叫做魔術方法(magic method),通常以兩個下劃線開頭和結尾,比如最常見的 __init__, __len__ 以及 __getitem__ 等,而 obj[key] 這樣的操作背後的特殊方法是 __getitem__,初始化一個類示例的時候,如 obj= Obj() 的操作背後,特殊方法就是 __init__。

通過實現 python 的這些特殊方法,可以讓自定義的對象實現和支持下面的操作:

  • 迭代
  • 集合類
  • 屬性訪問
  • 運算符重載
  • 函數和方法的調用
  • 對象的創建和銷燬
  • 字符串表示形式和格式化
  • 管理上下文(也就是 with 塊)

一摞 Python 風格的紙牌

接下來嘗試自定義一個類,並實現兩個特殊方法:__getitem__ 和 __len__ ,看看實現它們後,可以對自定義的類示例實現哪些操作。

這裡自定義一個紙牌類,並定義了數字和花色,代碼如下所示:

<code>import collections
# 用 nametuple 構建一個類來表示紙牌
Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = 'spades diamonds clubs hearts'.split()
    
    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, position):
        return self._cards[position]/<code>

其中輔助用到 collections 庫的 nametuple ,用來表示一張紙牌,其屬性包括數字 rank 和 花色 suit ,下面是對這個 Card 的簡單測試:

<code># 測試 Card
beer_card = Card('7', 'diamonds')
beer_card/<code>
python 數據模型

接著就是測試自定義的 FrenchDeck 類,這裡會調用 len() 方法看看一摞紙牌有多少張:

<code># 測試 FrenchDeck
deck = FrenchDeck()
len(deck)/<code>
python 數據模型

然後是進行索引訪問的操作,這裡測試從正序訪問第一張,以及最後一張紙牌的操作:

<code>print(deck[0], deck[-1])/<code>
python 數據模型

如果想進行隨機抽取卡牌,可以結合 random.choice 來實現:

<code># 隨機抽取,結合 random.choice
from random import choice

choice(deck)/<code>
python 數據模型

由於我們實現 __getitem__ 方法是獲取紙牌,所以也可以支持切片(slicing)的操作,例子如下所示:

<code># 切片
print(deck[:3])
print(deck[12::13])/<code>
python 數據模型

另外,實現 __getitem__ 方法就可以支持迭代操作:

<code># 可迭代的讀取
for card in deck:
    print(card)
    /<code>
python 數據模型

反向迭代也自然可以做到:

<code># 反向迭代
for card in reversed(deck):
    print(card)
    break/<code>
python 數據模型

另外,當然也可以自定義排序規則,如下所示:

<code># 制定排序的規則
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)

def spades_high(card):
    rank_value = FrenchDeck.ranks.index(card.rank)
    return rank_value * len(suit_values) + suit_values[card.suit]
 
# 對卡牌進行升序排序
for card in sorted(deck, key=spades_high):
    print(card)/<code>
python 數據模型

總結一下,實現 python 的特殊方法的好處包括:

  • 統一方面的名稱,如果有別人採用你自定義的類,不用花更多精力記住不同的名稱,比如獲取數量都是 len() 方法,而不會是 size 或者 length
  • 可以更加方便利用 python 的各種標準庫,比如 random.choice 、reversed、sorted ,不需要自己重新發明輪子

如何使用特殊方法

這裡分兩種情況來說明對於特殊方法的調用:

  1. python 內置的類型:比如列表(list)、字典(dict)等,那麼 CPython 會抄近路,即 __len__ 實際上會直接返回 PyVarObject 裡的 ob_size 屬性。 PyVarObject 是表示內存中長度可變的內置對象的 C 語言結構體,直接讀取這個值比調用一個方法要快很多
  2. 自定義的類:通過內置函數(如 len, iter, str 等)調用特殊方法是最好的選擇。

對於特殊方法的調用,這裡還要補充說明幾點:

  • 特殊方法的存在是為了被 Python 解釋器調用的。我們不需要調用它們,即不需要這麼寫 my_object.__len__(),而應該是 len(my_object),這裡的 my_object 表示一個自定義類的對象。
  • 通常對於特殊方法的調用都是隱式的。比如 for i in x 循環語句是用 iter(x) ,也就是調用 x.__iter__() 方法。
  • 除非有大量元編程存在,否則都不需要直接使用特殊方法;

接下來是實現一個自定義的二維向量類,然後自定義加號的特殊方法,實現運算符重載。

代碼例子如下所示:

<code># 一個簡單的二維向量類
from math import hypot

class Vector:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return 'Vector(%r, %r)' % (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, scalar):
        return Vector(self.x * scalar, self.y * scalar)/<code>

這裡除了必須實現的 __init__外,還實現了幾個特殊方法:

  • __add__: 加法運算符;
  • __bool__ :用於判斷是否真假,也就是在調用bool() 方法;默認情況下是自定義類的實例總是被認為是真的,但如果實現了 __bool__或者 __len__ ,則會返回它們的結果,bool()首先嚐試返回 __bool__ 方法,如果沒有實現,則會嘗試調用 __len__ 方法
  • __mul__ :實現的是標量乘法,即向量和數的乘法;
  • __abs__ :如果輸入是整數或者浮點數,返回輸入值的絕對值;如果輸入的是複數,返回這個複數的模;如果是輸入向量,返回的是它的模;
  • __repr__ : 可以將對象用字符串的形式表達出來;

這裡要簡單介紹下 __repr__ 和 __str__ 兩個方法的區別:

  • __repr__ :交互式控制檯、調試程序(debugger)、% 和 str.format 方法都會調用這個方法來獲取字符串形式;
  • __str__ :主要是在 str() 和 print() 方法中會調用該方法,它返回的字符串會對終端用戶更加友好;
  • 如果只想實現其中一個方法,__repr__ 是更好的選擇,因為默認會調用 __repr__ 方法。

接下來就是簡單測試這個類,測試結果如下所示:

python 數據模型

特殊方法一覽

下面分別根據是否和運算符相關分為兩類的特殊方法:

和運算符無關的特殊方法

類別 方法名 字符串/字節序列表現形式 __repr__, __str__,__format__,__bytes__ 數值轉換 __abs__,__bool__,__complex__,__int__,__float__,__hash__,__index__ 集合模擬 __len__,__getitem__,__setitem__,__delitem__,__contains__ 迭代枚舉 __iter__,__reversed__,__next__ 可調用模擬 __call__ 上下文管理 __enter__, __exit__ 實例創建和銷燬 __new__,__init__,__del__ 屬性管理 __getattr__,__getattribute__,__setattr__,__delattr__,__dir__ 屬性描述符 __get__,__set__,__delete__ 跟類相關的服務 __prepare__,__instancecheck__,__subclasscheck__

和運算符相關的特殊方法

類別 方法名和對應的運算符 一元運算符 __neg__ -, __pos__ +,__abs__ abs() 眾多比較運算符 __lt__ , __ge__ >= 算術運算符 __add__ +, __sub__ -, __mul__ *, __truediv__ /, __floordiv__ //, __mod__ %, __divmod__ divmod(), __pow__ **或者pow(), __round__ round() 反向算法運算符 __radd__, __rsub__, __rmul__, __rtruediv__, __rfloordiv__, __rmod__, __rdivmod__, __rpow__ 增量賦值算術運算符 __iadd__, __isub__, __imul__, __itruediv__, __ifloordiv__, __imod__, __ipow__ 位運算符 __invert__ ~, __lshift__ <>, __and__ &, __or__ |, __xor__ ^ 反向位運算符 __rlshift__, __rrshift__, __rand__, __rxor__, __ror__ 增量賦值位運算符 __ilshift__, __irshift__, __iand__, __ixor__, __ior__

這裡有兩類運算符要解釋一下:

  • 反向運算符:交換兩個操作數的位置的時候會調用反向運算符,比如 b * a 而不是 a * b ;
  • 增量賦值運算符:把一種中綴運算符變成賦值運算的捷徑,即是 a *= b 的操作

為什麼 len 不是普通方法

len 之所以不是普通方法,是為了讓 Python 自帶的數據結構變得高效,前面也提到內置類型在使用 len 方法的時候,CPython 會直接從一個 C 結構體裡讀取對象的長度,完全不會調用任何方法,因此速度會非常快。而在 python 的內置類型,比如列表 list、字符串 str、字典 dict 等查詢數量是非常常見的操作。

這種處理方式實際上是在保持內置類型的效率和保證語言的一致性之間找到一個平衡點。


小結

本文介紹了兩個代碼例子,說明了在自定義類的時候,實現特殊方法,可以實現和內置類型(比如列表、字典、字符串等)一樣的操作,包括實現迭代、運算符重載、打印類實例對象等,然後還根據是否和運算符相關將特殊方法分為兩類,並列舉出來了,最後也介紹了 len 方法的例子來說明 python 團隊是如何保持內置類型的效率和保證語言一致性的。


分享到:


相關文章: