Python學習之路20-數據模型

《流暢的Python》筆記。
本篇是Python進階篇的開始。本篇主要是對Python特殊方法的概述。

1. 前言

數據模型實際上是對Python框架的描述,它規範了這門語言自身構件模塊的接口,這些模塊包括但不限於序列、迭代器、函數、類和上下文管理器。無論在哪一種框架下寫程序,都會花費大量時間去實現那些會被框架自己調用的方法,Python也不例外。Python解釋器碰到特殊句法時,會使用特殊方法去激活一些基本的對象操做,這些特殊方法的名字以兩個下劃線開頭,以兩個下劃線結尾(因此特殊方法也叫雙下方法 dunder method),這些特殊方法名能讓本身編寫的對象實現和支持如下的語言構架,並與之交互:python

迭代、集合類、屬性訪問、運算符重載、函數和方法的調用、對象的建立和銷燬、字符串表示形式和格式化、管理上下文(即with塊)。程序員

下面經過一些例子來介紹經常使用的特殊方法。bash

2. Python風格紙牌

首先介紹兩個特殊方法__getitem____len__這兩個特殊方法。如下代碼建立了一個紙牌類:微信

import collections

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]

namedtuple,即命名元組,相似於C/C++中的struct,定義以下:數據結構

collections.namedtuple(typename, field_names, verbose=False, rename=False)框架

第一個參數是元組名;第二個是該元組中含的屬性名;第三個參數表示在構建該命名元組以前先打印出該命名元組的結構,若是在控制檯輸入第3行代碼,並置verboseTrue的話,會輸出該命名元組的內部結構,實際上它是一個繼承自tuple的類,因爲輸出過長,請你們自行實驗;若是該命名元組的元素名中有Python關鍵字,則須要置第四個參數爲True,這些與關鍵字重名的元素名會被特殊處理。dom

用命名元組建立一個不帶方法的對象十分簡單:函數

>>> from chapter20 import Card, FrenchDeck
>>> beer_card = Card("7", "diamonds")
>>> beer_card
Card(rank='7', suit='diamonds')

因爲FrenchDeck實現了__getitem__方法,因此能夠像操做ListTuple同樣操做FrenchDeck,好比隨機訪問,切片:網站

>>> deck = FrenchDeck()
>>> len(deck)
52
>>> deck[0]
Card(rank='2', suit='spades')
>>> deck[-1]
Card(rank='A', suit='hearts')
>>> from random import choice
>>> choice(deck)
Card(rank='4', suit='clubs')
>>> choice(deck)
Card(rank='J', suit='clubs')
>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades')]
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'), 
Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]

因爲實現了該方法,FrenchDeck仍是個可迭代對象,便可以用for循環對其訪問(也能夠反向訪問reversed):ui

>>> for card in deck:
>>> ... print(card)
    
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
-- snip --
Card(rank='Q', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='A', suit='hearts')

迭代一般是隱式的,譬如說一個集合類型沒有實現__contains__方法,那麼in運算符就會按順序作一次迭代搜索(調用__getitem__),因而in運算符能夠用在FrenchDeck上:

>>> Card('2', 'spades') in deck
True

若是對上述deck變量調用sorted函數,Python將按ASCII碼進行排序,但這並非撲克牌的正確排序,因此下面咱們自定義排序方法:

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)

此時輸出的結果就是先按點數排序,再按花色排序。

3. 如何使用特殊方法

須要明確一點,特殊方法的存在是爲了給Python解釋器調用到,做爲程序員並不須要調用他們,也便是說,沒有my_object.__len__()這種寫法,而應該是len(my_object)。說到__len__方法,若是是Python內置類型,CPython會抄個近路,該方法實際上會直接返回PyVarObject裏的ob_size屬性,而PyVarObject是表示內存中長度可變的內痔對象的C語言結構體。

不少時候特殊方法的調用是隱式的,好比for i in x:這個語句,背後其實用的是iter(x),而這個函數的背後則是x.__iter__()方法,固然前提是這個方法在x中被實現(若是沒被實現則會調用__getitem__方法)。

直接調用這個值比調用一個方法快不少。直接調用特殊方法的頻率應該遠遠低於你去實現它們的次數。

經過內置的函數(例如leniterstr等)來使用特殊方法是最好的選擇。這些內置函數不只會調用特殊方法,一般還提供額外的好處,並且對於內置的類來講,它們的速度更快。

還有一點值得注意:不要想固然地隨意添加特殊方法,好比__foo__之類的,由於雖然如今這個名字沒有被Python內部使用,之後就不必定了。

3.1 自定義向量Vector

使用5個特殊方法實現Vector的字符串輸出,取絕對值(若是是複數則是取模),返回布爾值,加法和數乘等運算:

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)

    # 在Python中,只有0,NULL纔是False,其他均爲True
    def __bool__(self):
        # 更簡單的寫法是:
        # return bool(self.x or self.y)
        return bool(abs(self))
    
    # 實現加法運算符重載
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    # 實現乘法運算符重載,這裏是數乘,且尚未實現交換律(須要實現__rmul__方法)
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

Python有一個內置函數叫作repr。該函數經過特殊方法__repr__來獲得一個對象的字符串表示形式,若是沒有該特殊方法,當咱們在控制檯打印一個向量對象時,獲得的字符串多是<Vector object at 0x10e00070>

# 代碼:
v1 = Vector(2, 4)
v2 = Vector(2, 1)
print(v1 + v2)
print(abs(v1))
print(v1 * 3)

# 結果:
Vector(4, 5)
4.47213595499958
Vector(6, 12)

__repr____str__的區別與聯繫:前者方便咱們調試和記錄日誌,後者則是給終端用戶看的。後者是在str()函數被使用,或者是在print函數打印一個對象的時候才被調用,它返回的字符串對終端用戶友好。若是隻想實現這兩個特殊方法中的一個,__repr__是更好的選擇,由於若是一個對象沒有__str__函數,Python又須要調用它時,解釋器會用__repr__代替。

上述Vector類實現了__bool__方法,它可用於須要布爾值的上下文中(if, while, and, or, not等)。默認狀況下,咱們本身定義的類的實例總被認爲是True,除非重寫了這個類的__bool____len__方法。bool(x)的背後是調用x.__bool__();若是不存在__bool__方法,那麼bool(x)會嘗試調用x.__len__(),若是該方法返回0,則bool返回False,不然返回True

3.2 爲何len不是普通方法

「實用勝於純粹」(Python之禪裏的一句話)。len之因此不是一個普通方法,是爲了讓Python自帶的數據結構能夠走後門,abs也是同理。但多虧了它是特殊方法,咱們也能夠把len用於自定義數據類型。這種處理方式在保持內置類型的效率和保證語言的一致性之間找到了一個平衡點,也印證了「Python之禪」中的另外一句話:「不能讓特例特殊到考試破壞既定規則」。

4. 總結

經過實現特殊方法,自定義數據類型能夠表現得跟內置類型同樣,從而讓咱們寫出更具Python風格(Pythonic)的代碼。後面的內容將圍繞更多的特殊方法展開。


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

相關文章
相關標籤/搜索