草根學Python(十)Python 的 Magic Method

兩點水發表於2017-08-17

前言

距離上一篇已經三個多星期了,最近比較累,下班回到家,很早就休息了,所以更新的進度有點慢。

目錄

草根學Python(十) Python 的 Magic Method
草根學Python(十) Python 的 Magic Method

一、Python 的 Magic Method

在 Python 中,所有以 "" 雙下劃線包起來的方法,都統稱為"魔術方法"。比如我們接觸最多的 `init__` 。魔術方法有什麼作用呢?

使用這些魔術方法,我們可以構造出優美的程式碼,將複雜的邏輯封裝成簡單的方法。

那麼一個類中有哪些魔術方法呢?

我們可以使用 Python 內建的方法 dir() 來列出類中所有的魔術方法.示例如下:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

class User(object):
    pass


if __name__ == '__main__':
    print(dir(User()))複製程式碼

輸出的結果:

Python 類的魔術方法
Python 類的魔術方法

可以看到,一個類的魔術方法還是挺多的,截圖也沒有截全,不過我們只需要瞭解一些常見和常用的魔術方法就好了。

二、構造(__new__)和初始化(__init__)

通過上一篇的內容,我們已經知道定義一個類時,我們經常會通過 __init__(self) 的方法在例項化物件的時候,對屬性進行設定。比如下面的例子:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

class User(object):
    def __init__(self, name, age):
        self.name = name;
        self.age = age;

user=User('兩點水',23)複製程式碼

實際上,建立一個類的過程是分為兩步的,一步是建立類的物件,還有一步就是對類進行初始化。__new__ 是用來建立類並返回這個類的例項, 而__init__ 只是將傳入的引數來初始化該例項.__new__ 在建立一個例項的過程中必定會被呼叫,但 __init__ 就不一定,比如通過pickle.load 的方式反序列化一個例項時就不會呼叫 __init__ 方法。

Python類建立的過程
Python類建立的過程

def __new__(cls) 是在 def __init__(self) 方法之前呼叫的,作用是返回一個例項物件。還有一點需要注意的是:__new__ 方法總是需要返回該類的一個例項,而 __init__ 不能返回除了 None 的任何值

具體的示例:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

class User(object):
    def __new__(cls, *args, **kwargs):
        # 列印 __new__方法中的相關資訊
        print('呼叫了 def __new__ 方法')
        print(args)
        # 最後返回父類的方法
        return super(User, cls).__new__(cls)

    def __init__(self, name, age):
        print('呼叫了 def __init__ 方法')
        self.name = name
        self.age = age


if __name__ == '__main__':
    usr = User('兩點水', 23)複製程式碼

看看輸出的結果:

呼叫了 def __new__ 方法
('兩點水', 23)
呼叫了 def __init__ 方法複製程式碼

通過列印的結果來看,我們就可以知道一個類建立的過程是怎樣的了,先是呼叫了 __new__ 方法來建立一個物件,把引數傳給 __init__ 方法進行例項化。

其實在實際開發中,很少會用到 __new__ 方法,除非你希望能夠控制類的建立。通常講到 __new__ ,都是牽扯到 metaclass(元類)的。

當然當一個物件的生命週期結束的時候,解構函式 __del__ 方法會被呼叫。但是這個方法是 Python 自己對物件進行垃圾回收的。

三、屬性的訪問控制

之前也有講到過,Python 沒有真正意義上的私有屬性。然後這就導致了對 Python 類的封裝性比較差。我們有時候會希望 Python 能夠定義私有屬性,然後提供公共可訪問的 get 方法和 set 方法。Python 其實可以通過魔術方法來實現封裝。

方法 說明
__getattr__(self, name) 該方法定義了你試圖訪問一個不存在的屬性時的行為。因此,過載該方法可以實現捕獲錯誤拼寫然後進行重定向, 或者對一些廢棄的屬性進行警告。
__setattr__(self, name, value) 定義了對屬性進行賦值和修改操作時的行為。不管物件的某個屬性是否存在,都允許為該屬性進行賦值.有一點需要注意,實現 __setattr__ 時要避免"無限遞迴"的錯誤,
__delattr__(self, name) __delattr____setattr__ 很像,只是它定義的是你刪除屬性時的行為。實現 __delattr__ 是同時要避免"無限遞迴"的錯誤
__getattribute__(self, name) __getattribute__ 定義了你的屬性被訪問時的行為,相比較,__getattr__ 只有該屬性不存在時才會起作用。因此,在支援 __getattribute__的 Python 版本,呼叫__getattr__ 前必定會呼叫 __getattribute__``__getattribute__ 同樣要避免"無限遞迴"的錯誤。

通過上面的方法表可以知道,在進行屬性訪問控制定義的時候你可能會很容易的引起一個錯誤,可以看看下面的示例:

def __setattr__(self, name, value):
    self.name = value
    # 每當屬性被賦值的時候, ``__setattr__()`` 會被呼叫,這樣就造成了遞迴呼叫。
    # 這意味這會呼叫 ``self.__setattr__('name', value)`` ,每次方法會呼叫自己。這樣會造成程式崩潰。

def __setattr__(self, name, value):
    # 給類中的屬性名分配值
    self.__dict__[name] = value  
    # 定製特有屬性複製程式碼

上面方法的呼叫具體示例如下:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

class User(object):
    def __getattr__(self, name):
        print('呼叫了 __getattr__ 方法')
        return super(User, self).__getattr__(name)

    def __setattr__(self, name, value):
        print('呼叫了 __setattr__ 方法')
        return super(User, self).__setattr__(name, value)

    def __delattr__(self, name):
        print('呼叫了 __delattr__ 方法')
        return super(User, self).__delattr__(name)

    def __getattribute__(self, name):
        print('呼叫了 __getattribute__ 方法')
        return super(User, self).__getattribute__(name)


if __name__ == '__main__':
    user = User()
    # 設定屬性值,會呼叫 __setattr__
    user.attr1 = True
    # 屬性存在,只有__getattribute__呼叫
    user.attr1
    try:
        # 屬性不存在, 先呼叫__getattribute__, 後呼叫__getattr__
        user.attr2
    except AttributeError:
        pass
    # __delattr__呼叫
    del user.attr1複製程式碼

輸出的結果:

呼叫了 __setattr__ 方法
呼叫了 __getattribute__ 方法
呼叫了 __getattribute__ 方法
呼叫了 __getattr__ 方法
呼叫了 __delattr__ 方法複製程式碼

四、物件的描述器

一般來說,一個描述器是一個有“繫結行為”的物件屬性 (object attribute),它的訪問控制被描述器協議方法重寫。這些方法是 __get__(), __set__() , 和 __delete__() 。有這些方法的物件叫做描述器。

預設對屬性的訪問控制是從物件的字典裡面 (__dict__) 中獲取 (get) , 設定 (set) 和刪除 (delete) 。舉例來說, a.x 的查詢順序是, a.__dict__['x'] , 然後 type(a).__dict__['x'] , 然後找 type(a) 的父類 ( 不包括元類 (metaclass) ).如果查詢到的值是一個描述器, Python 就會呼叫描述器的方法來重寫預設的控制行為。這個重寫發生在這個查詢環節的哪裡取決於定義了哪個描述器方法。注意, 只有在新式類中時描述器才會起作用。在之前的篇節中已經提到新式類和舊式類的,有興趣可以檢視之前的篇節來看看,至於新式類最大的特點就是所有類都繼承自 type 或者 object 的類。

在物件導向程式設計時,如果一個類的屬性有相互依賴的關係時,使用描述器來編寫程式碼可以很巧妙的組織邏輯。在 Django 的 ORM 中,models.Model中的 InterField 等欄位, 就是通過描述器來實現功能的。

我們先看下下面的例子:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

class User(object):
    def __init__(self, name='兩點水', sex='男'):
        self.sex = sex
        self.name = name

    def __get__(self, obj, objtype):
        print('獲取 name 值')
        return self.name

    def __set__(self, obj, val):
        print('設定 name 值')
        self.name = val


class MyClass(object):
    x = User('兩點水', '男')
    y = 5


if __name__ == '__main__':
    m = MyClass()
    print(m.x)

    print('\n')

    m.x = '三點水'
    print(m.x)

    print('\n')

    print(m.x)

    print('\n')

    print(m.y)複製程式碼

輸出的結果如下:

獲取 name 值
兩點水


設定 name 值
獲取 name 值
三點水


獲取 name 值
三點水


5複製程式碼

通過這個例子,可以很好的觀察到這 __get__()__set__() 這些方法的呼叫。

再看一個經典的例子

我們知道,距離既可以用單位"米"表示,也可以用單位"英尺"表示。
現在我們定義一個類來表示距離,它有兩個屬性: 米和英尺。

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-


class Meter(object):
    def __init__(self, value=0.0):
        self.value = float(value)

    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        self.value = float(value)


class Foot(object):
    def __get__(self, instance, owner):
        return instance.meter * 3.2808

    def __set__(self, instance, value):
        instance.meter = float(value) / 3.2808


class Distance(object):
    meter = Meter()
    foot = Foot()


if __name__ == '__main__':
    d = Distance()
    print(d.meter, d.foot)
    d.meter = 1
    print(d.meter, d.foot)
    d.meter = 2
    print(d.meter, d.foot)複製程式碼

輸出的結果:

0.0 0.0
1.0 3.2808
2.0 6.5616複製程式碼

在上面例子中,在還沒有對 Distance 的例項賦值前, 我們認為 meter 和 foot 應該是各自類的例項物件, 但是輸出卻是數值。這是因為 __get__ 發揮了作用.

我們只是修改了 meter ,並且將其賦值成為 int ,但 foot 也修改了。這是 __set__ 發揮了作用.

描述器物件 (Meter、Foot) 不能獨立存在, 它需要被另一個所有者類 (Distance) 所持有。描述器物件可以訪問到其擁有者例項的屬性,比如例子中 Foot 的 instance.meter

五、自定義容器(Container)

經過之前編章的介紹,我們知道在 Python 中,常見的容器型別有: dict, tuple, list, string。其中也提到過可容器和不可變容器的概念。其中 tuple, string 是不可變容器,dict, list 是可變容器。 可變容器和不可變容器的區別在於,不可變容器一旦賦值後,不可對其中的某個元素進行修改。當然具體的介紹,可以看回之前的文章,有圖文介紹。

那麼這裡先提出一個問題,這些資料結構就夠我們開發使用嗎?不夠的時候,或者說有些特殊的需求不能單單隻使用這些基本的容器解決的時候,該怎麼辦呢?

這個時候就需要自定義容器了,那麼具體我們該怎麼做呢?

功能 說明
自定義不可變容器型別 需要定義 __len____getitem__ 方法
自定義可變型別容器 在不可變容器型別的基礎上增加定義 __setitem____delitem__
自定義的資料型別需要迭代 需定義 __iter__
返回自定義容器的長度 需實現 __len__(self)
自定義容器可以呼叫 self[key] ,如果 key 型別錯誤,丟擲TypeError ,如果沒法返回key對應的數值時,該方法應該丟擲ValueError 需要實現 __getitem__(self, key)
當執行 self[key] = value 呼叫是 __setitem__(self, key, value)這個方法
當執行 del self[key] 方法 其實呼叫的方法是 __delitem__(self, key)
當你想你的容器可以執行 for x in container: 或者使用 iter(container) 需要實現 __iter__(self) ,該方法返回的是一個迭代器

來看一下使用上面魔術方法實現 Haskell 語言中的一個資料結構:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

class FunctionalList:
    ''' 實現了內建型別list的功能,並豐富了一些其他方法: head, tail, init, last, drop, take'''

    def __init__(self, values=None):
        if values is None:
            self.values = []
        else:
            self.values = values

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

    def __getitem__(self, key):
        return self.values[key]

    def __setitem__(self, key, value):
        self.values[key] = value

    def __delitem__(self, key):
        del self.values[key]

    def __iter__(self):
        return iter(self.values)

    def __reversed__(self):
        return FunctionalList(reversed(self.values))

    def append(self, value):
        self.values.append(value)

    def head(self):
        # 獲取第一個元素
        return self.values[0]

    def tail(self):
        # 獲取第一個元素之後的所有元素
        return self.values[1:]

    def init(self):
        # 獲取最後一個元素之前的所有元素
        return self.values[:-1]

    def last(self):
        # 獲取最後一個元素
        return self.values[-1]

    def drop(self, n):
        # 獲取所有元素,除了前N個
        return self.values[n:]

    def take(self, n):
        # 獲取前N個元素
        return self.values[:n]複製程式碼

六、運算子相關的魔術方法

運算子相關的魔術方法實在太多了,j就大概列舉下面兩類:

1、比較運算子

魔術方法 說明
__cmp__(self, other) 如果該方法返回負數,說明 self < other; 返回正數,說明 self > other; 返回 0 說明 self == other。強烈不推薦來定義 __cmp__ , 取而代之, 最好分別定義 __lt__, __eq__ 等方法從而實現比較功能。 __cmp__ 在 Python3 中被廢棄了。
__eq__(self, other) 定義了比較操作符 == 的行為
__ne__(self, other) 定義了比較操作符 != 的行為
__lt__(self, other) 定義了比較操作符 < 的行為
__gt__(self, other) 定義了比較操作符 > 的行為
__le__(self, other) 定義了比較操作符 <= 的行為
__ge__(self, other) 定義了比較操作符 >= 的行為

來看個簡單的例子就能理解了:

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

class Number(object):
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        print('__eq__')
        return self.value == other.value

    def __ne__(self, other):
        print('__ne__')
        return self.value != other.value

    def __lt__(self, other):
        print('__lt__')
        return self.value < other.value

    def __gt__(self, other):
        print('__gt__')
        return self.value > other.value

    def __le__(self, other):
        print('__le__')
        return self.value <= other.value

    def __ge__(self, other):
        print('__ge__')
        return self.value >= other.value


if __name__ == '__main__':
    num1 = Number(2)
    num2 = Number(3)
    print('num1 == num2 ? --------> {} \n'.format(num1 == num2))
    print('num1 != num2 ? --------> {} \n'.format(num1 == num2))
    print('num1 < num2 ? --------> {} \n'.format(num1 < num2))
    print('num1 > num2 ? --------> {} \n'.format(num1 > num2))
    print('num1 <= num2 ? --------> {} \n'.format(num1 <= num2))
    print('num1 >= num2 ? --------> {} \n'.format(num1 >= num2))複製程式碼

輸出的結果為:

__eq__
num1 == num2 ? --------> False 

__eq__
num1 != num2 ? --------> False 

__lt__
num1 < num2 ? --------> True 

__gt__
num1 > num2 ? --------> False 

__le__
num1 <= num2="" ?="" --------=""> True 

__ge__
num1 >= num2 ? --------> False=>複製程式碼

2、算術運算子

魔術方法 說明
__add__(self, other) 實現了加號運算
__sub__(self, other) 實現了減號運算
__mul__(self, other) 實現了乘法運算
__floordiv__(self, other) 實現了 // 運算子
___div__(self, other) 實現了/運算子. 該方法在 Python3 中廢棄. 原因是 Python3 中,division 預設就是 true division
__truediv__(self, other) 實現了 true division. 只有你宣告瞭 from __future__ import division 該方法才會生效
__mod__(self, other) 實現了 % 運算子, 取餘運算
__divmod__(self, other) 實現了 divmod() 內建函式
__pow__(self, other) 實現了 ** 操作. N 次方操作
__lshift__(self, other) 實現了位操作 <<
__rshift__(self, other) 實現了位操作 >>
__and__(self, other) 實現了位操作 &
__or__(self, other) 實現了位操作 ` `
__xor__(self, other) 實現了位操作 ^

最後,如果對本文感興趣的,可以關注下公眾號:

公眾號
公眾號

相關文章