魔法方法推開Python進階學習大門

dongfanger發表於2021-01-15

熱愛Python

Python是Guido van Rossum設計出來的讓使用者覺得如沐春風的一門程式語言。2020年11月12日,64歲的Python之父宣佈由於退休生活太無聊,自己決定加入Microsoft的DevDiv Team,致力於“確保更好地使用Python”。儘管在國內有些聲音在Diss著Python,認為它太簡單,只是個指令碼語言,但是它的發明者對Python的熱情,仍然激勵著我們堅持對Python的熱愛。

龜叔是所有程式語言發明者當中頭髮最多的這位。

Guido-portrait-2014-curvves

奇蹟時刻

collection.len()是面嚮物件語言的寫法,len(collection)是Python語言的寫法,這種風格叫做Pythonic。從前者到後者,就像變魔術一樣,一瞬間讓人眼前一亮。這個魔術就是Python魔法方法,或者叫雙下方法,它是用雙下劃線開頭和雙下劃線結尾的特殊方法,比如obj[key],Python直譯器實際上會轉換成obj.__getitem__(key)來執行,但是使用者並無感知。

__getitem____len__

__getitem__用來獲取資料,__len__用來返回長度,這2個魔法方法是Python基礎,我們通過一副撲克牌來了解:

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]

本來我們對這副牌什麼都不能做,但是由於實現了__len__,可以使用len()函式檢視有多少張牌:

>>> len(deck)
52

由於實現了__getitem__,可以使用中括號索引取值:

>>> deck[0]
Card(rank='2', suit='spades')

能進行切片:

>>> 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')]

能迭代:

>>> for card in deck:  # doctest: +ELLIPSIS
...   print(card)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
...

發現沒有,魔法方法是可以用來裝B的!別人寫個類只能getset,你寫個類還能花式炫技,666。

Python魔法方法是給Python直譯器使用的,一般不需要直接呼叫,Python會自己去調,比如把len(my_object)寫成my_object.__len__(),就弄巧成拙了。

魔法方法實現運算子

前面例子實現了取值和長度,接著再看一個例子,使用__repr____abs____bool____add____mul__,實現運算子:

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)

__add__實現了加法:

>>> v1 = Vector(2, 4)
>>> v2 = Vector(2, 1)
>>> v1 + v2
Vector(4, 5)

__abs__實現了絕對值:

>>> v = Vector(3, 4)
>>> abs(v)
5.0

__mul__實現了乘法:

>>> v * 3
Vector(9, 12)

__repr__實現了物件的字串表示:

Vector(4, 5)

否則得到的字串可能是<Vector object at 0x10e100070>

__bool__實現了布林值:

if Vector(4, 5):
    return True

其他魔法方法

一篇文章是講不完魔法方法的,我們會在後續文章中,繼續探討如何使用和實現它們。

Tips

本小節內容是我看《流暢的Python》第一遍時記錄的知識點:

  1. collections.namedtuple可以用來建立只有少數屬性但沒有方法的物件,比如

    beer_card = Card('7', 'diamonds')
    
  2. random.choicerandom.sample不一樣的地方在於,sample是返回序列,choice是返回元素,當使用sample(list, 1)[0]的時候,不如直接使用choice(list)。

  3. 特殊方法的存在是為了被Python直譯器呼叫的。

  4. PyVarObject是表示記憶體中長度可變的內建物件的C語言結構體。liststrbytearray的__len__實際上返回的PyVarObject.ob_size屬性,這個比呼叫一個方法要快的多。

  5. len之所以不是一個普通方法,是為了讓python自帶的資料結構可以走後門,abs也是同理。

  6. 很多時候呼叫__init__方法的目的是,在你自己的子類的__init__方法中呼叫超類的構造器。

  7. abs,如果輸入是整數或者浮點數,它返回的是輸入值的絕對值;如果輸入是複數,那麼返回這個複數的模。

  8. __repr__和__str__二選一的話,__repr__更好,因為如果一個物件沒有__str__函式,直譯器會用__repr__作為替代。

  9. python物件的一個基本要求就是它得有合理的字串表示形式,這就是資料模型中存在特殊方法__repr__和__str__的原因。

  10. 為了判定一個值x為真還是為假,python會呼叫bool(x),它的背後是呼叫x.__bool__()。如果不存在,就會呼叫x.__len__(),返回0為Flase,非0為True。

  11. python通過運算子過載這一模式提供了豐富的數值型別,除了內建那些,還有decimal.Decimalfractions.Fraction

小結

本文是Python進階系列開篇,參考《流暢的Python》序章改寫而成。原書內容有深度有廣度,我選擇了其中的魔法方法知識點,作為切入,循序漸進學習。其實書中這一章節的副標題是“資料模型”,它是個什麼概念呢?

系列文章會不定期同步到線上電子書中,歡迎訪問檢視:

https://dongfanger.gitee.io/blog/

參考資料:

《流暢的Python》

https://docs.python.org/3/reference/datamodel.html

相關文章