歡迎光臨
每天分享高質量文章

Python 開發者不得不知的魔術方法(Magic Method)


來源:j_hao104

my.oschina.net/jhao104/blog/779743

介紹

在Python中,所有以“__”雙下劃線包起來的方法,都統稱為“Magic Method”,例如類的初始化方法 __init__,Python中所有的魔術方法均在官方檔案中有相應描述,但是對於官方的描述比較混亂而且組織比較鬆散。很難找到有一個例子。

構造和初始化

每個Pythoner都知道一個最基本的魔術方法, __init__ 。透過此方法我們可以定義一個物件的初始操作。然而,當呼叫 x = SomeClass() 的時候, __init__ 並不是第一個被呼叫的方法。實際上,還有一個叫做__new__ 的方法,兩個共同構成了“建構式”。

__new__是用來建立類並傳回這個類的實體, 而__init__只是將傳入的引數來初始化該實體。

在物件生命週期呼叫結束時,__del__ 方法會被呼叫,可以將__del__理解為“構析函式”。下麵透過程式碼的看一看這三個方法:

from os.path import join

class FileObject:
   '''給檔案物件進行包裝從而確認在刪除時檔案流關閉'''

   def __init__(self, filepath='~', filename='sample.txt'):
       #讀寫樣式開啟一個檔案
       self.file = open(join(filepath, filename), 'r+')

   def __del__(self):
       self.file.close()
       del self.file

控制屬性訪問

許多從其他語言轉到Python的人會抱怨它缺乏類的真正封裝。(沒有辦法定義私有變數,然後定義公共的getter和setter)。Python其實可以透過魔術方法來完成封裝。我們來看一下:

__getattr__(self, name)

定義當使用者試圖獲取一個不存在的屬性時的行為。這適用於對普通拼寫錯誤的獲取和重定向,對獲取一些不建議的屬性時候給出警告(如果你願意你也可以計算並且給出一個值)或者處理一個 AttributeError 。只有當呼叫不存在的屬性的時候會被傳回。

__setattr__(self, name, value)

與__getattr__(self, name)不同,__setattr__ 是一個封裝的解決方案。無論屬性是否存在,它都允許你定義對對屬性的賦值行為,以為這你可以對屬性的值進行個性定製。實現__setattr__時要避免”無限遞迴”的錯誤。

__delattr__

與 __setattr__ 相同,但是功能是刪除一個屬性而不是設定他們。實現時也要防止無限遞迴現象發生。

__getattribute__(self, name)

__getattribute__定義了你的屬性被訪問時的行為,相比較,__getattr__只有該屬性不存在時才會起作用。因此,在支援__getattribute__的Python版本,呼叫__getattr__前必定會呼叫 __getattribute__。__getattribute__同樣要避免”無限遞迴”的錯誤。需要提醒的是,最好不要嘗試去實現__getattribute__,因為很少見到這種做法,而且很容易出bug。

在進行屬性訪問控制定義的時候很可能會很容易引起“無限遞迴”。如下麵程式碼:

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

#  正確用法
def __setattr__(self, name, value):
   self.__dict__[name] = value  # 給類中的屬性名分配值
   # 定製特有屬性

Python的魔術方法很強大,但是用時卻需要慎之又慎,瞭解正確的使用方法非常重要。

建立自定義容器

有很多方法可以讓你的Python類行為向內置容器型別一樣,比如我們常用的list、dict、tuple、string等等。Python的容器型別分為可變型別(如list、dict)和不可變型別(如string、tuple),可變容器和不可變容器的區別在於,不可變容器一旦賦值後,不可對其中的某個元素進行修改。   在講建立自定義容器之前,應該先瞭解下協議。這裡的協議跟其他語言中所謂的”介面”概念很像,它給你很多你必須定義的方法。然而在Python中的協議是很不正式的,不需要明確宣告實現。事實上,他們更像一種指南。

自定義容器的magic method

下麵細緻瞭解下定義容器可能用到的魔術方法。首先,實現不可變容器的話,你只能定義 __len__ 和 __getitem__ (下麵會講更多)。可變容器協議則需要所有不可變容器的所有,另外還需要 __setitem__ 和 __delitem__。如果你希望你的物件是可迭代的話,你需要定義 __iter__ 會傳回一個迭代器。迭代器必須遵循迭代器協議,需要有 __iter__(傳回它本身) 和 next。

__len__(self)

傳回容器的長度。對於可變和不可變容器的協議,這都是其中的一部分。

__getitem__(self, key)

定義當某一項被訪問時,使用self[key]所產生的行為。這也是不可變容器和可變容器協議的一部分。如果鍵的型別錯誤將產生TypeError;如果key沒有合適的值則產生KeyError。

__setitem__(self, key, value)

當你執行self[key] = value時,呼叫的是該方法。

__delitem__(self, key)

定義當一個專案被刪除時的行為(比如 del self[key])。這隻是可變容器協議中的一部分。當使用一個無效的鍵時應該丟擲適當的異常。

__iter__(self)

傳回一個容器迭代器,很多情況下會傳回迭代器,尤其是當內建的iter()方法被呼叫的時候,以及當使用for x in container:方式迴圈的時候。迭代器是它們本身的物件,它們必須定義傳回self的__iter__方法。

__reversed__(self)

實現當reversed()被呼叫時的行為。應該傳回序列反轉後的版本。僅當序列可以是有序的時候實現它,例如對於串列或者元組。

__contains__(self, item)

定義了呼叫in和not in來測試成員是否存在的時候所產生的行為。你可能會問為什麼這個不是序列協議的一部分?因為當__contains__沒有被定義的時候,如果沒有定義,那麼Python會迭代容器中的元素來一個一個比較,從而決定傳回True或者False。

__missing__(self, key)

dict字典型別會有該方法,它定義了key如果在容器中找不到時觸發的行為。比如d = {‘a’: 1}, 當你執行d[notexist]時,d.__missing__[‘notexist’]就會被呼叫。

一個例子

下麵是書中的例子,用魔術方法來實現Haskell語言中的一個資料結構。

# -*- 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]

其實在collections模組中已經有了很多類似的實現,比如Counter、OrderedDict等等。

反射

你也可以控制怎麼使用內建在函式sisinstance()和issubclass()方法 反射定義魔術方法. 這個魔術方法是:

__instancecheck__(self, instance)

檢查一個實體是不是你定義的類的實體

__subclasscheck__(self, subclass)

檢查一個類是不是你定義的類的子類

這些魔術方法的用例看起來很小, 並且確實非常實用. 它們反應了關於面向物件程式上一些重要的東西在Python上,並且總的來說Python: 總是一個簡單的方法去找某些事情, 即使是沒有必要的. 這些魔法方法可能看起來不是很有用, 但是一旦你需要它們,你會感到慶幸它們的存在。

可呼叫的物件

你也許已經知道,在Python中,方法是最高階的物件。這意味著他們也可以被傳遞到方法中,就像其他物件一樣。這是一個非常驚人的特性。

在Python中,一個特殊的魔術方法可以讓類的實體的行為表現的像函式一樣,你可以呼叫它們,將一個函式當做一個引數傳到另外一個函式中等等。這是一個非常強大的特性,其讓Python程式設計更加舒適甜美。

__call__(self, [args…])

允許一個類的實體像函式一樣被呼叫。實質上說,這意味著 x() 與 x.__call__() 是相同的。註意 __call__ 的引數可變。這意味著你可以定義 __call__ 為其他你想要的函式,無論有多少個引數。

__call__ 在那些類的實體經常改變狀態的時候會非常有效。呼叫這個實體是一種改變這個物件狀態的直接和優雅的做法。用一個實體來表達最好不過了:

# -*- coding: UTF-8 -*-
class Entity:
   """
   呼叫物體來改變物體的位置
   """


def __init__(self, size, x, y):
   self.x, self.y = x, y
   self.size = size

def __call__(self, x, y):
   """
   改變物體的位置
   """

   self.x, self.y = x, y

背景關係管理

with宣告是從Python2.5開始引進的關鍵詞。你應該遇過這樣子的程式碼:

with open('foo.txt') as bar:
   # do something with bar

在with宣告的程式碼段中,我們可以做一些物件的開始操作和退出操作,還能對異常進行處理。這需要實現兩個魔術方法: __enter__ 和 __exit__。

__enter__(self)

定義了當使用with陳述句的時候,會話管理器在塊被初始建立時要產生的行為。請註意,__enter__的傳回值與with陳述句的標的或者as後的名字系結。

__exit__(self, exception_type, exception_value, traceback)

定義了當一個程式碼塊被執行或者終止後,會話管理器應該做什麼。它可以被用來處理異常、執行清理工作或做一些程式碼塊執行完畢之後的日常工作。如果程式碼塊執行成功,exceptiontype,exceptionvalue,和traceback將會為None。否則,你可以選擇處理這個異常或者是直接交給使用者處理。如果你想處理這個異常的話,請確保__exit在所有陳述句結束之後傳回True。如果你想讓異常被會話管理器處理的話,那麼就讓其產生該異常。

建立物件描述器

描述器是透過獲取、設定以及刪除的時候被訪問的類。當然也可以改變其它的物件。描述器並不是獨立的。相反,它意味著被一個所有者類持有。當建立面向物件的資料庫或者類,裡面含有相互依賴的屬相時,描述器將會非常有用。一種典型的使用方法是用不同的單位表示同一個數值,或者表示某個資料的附加屬性。

為了成為一個描述器,一個類必須至少有__get__,__set__,__delete__方法被實現:

__get__(self, instance, owner)

定義了當描述器的值被取得的時候的行為。instance是擁有該描述器物件的一個實體。owner是擁有者本身

__set__(self, instance, value)

定義了當描述器的值被改變的時候的行為。instance是擁有該描述器類的一個實體。value是要設定的值。

__delete__(self, instance)

定義了當描述器的值被刪除的時候的行為。instance是擁有該描述器物件的一個實體。

下麵是一個描述器的實體:單位轉換。

# -*- 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(10)
   foot = Foot()

使用時:

>>>d = Distance()
>>>print d.foot
>>>print d.meter
32.808
10.0

複製

有時候,尤其是當你在處理可變物件時,你可能想要複製一個物件,然後對其做出一些改變而不希望影響原來的物件。這就是Python的copy所發揮作用的地方。

__copy__(self)

定義了當對你的類的實體呼叫copy.copy()時所產生的行為。copy.copy()傳回了你的物件的一個淺複製——這意味著,當實體本身是一個新實體時,它的所有資料都被取用了——例如,當一個物件本身被覆制了,它的資料仍然是被取用的(因此,對於淺複製中資料的更改仍然可能導致資料在原始物件的中的改變)。

__deepcopy__(self, memodict={})

定義了當對你的類的實體呼叫copy.deepcopy()時所產生的行為。copy.deepcopy()傳回了你的物件的一個深複製——物件和其資料都被複製了。memodict是對之前被複製的物件的一個快取——這優化了複製過程並且阻止了對遞迴資料結構複製時的無限遞迴。當你想要進行對一個單獨的屬性進行深複製時,呼叫copy.deepcopy(),並以memodict為第一個引數。

附錄

用於比較的魔術方法

數值計算的魔術方法

單目運運算元和函式

雙目運運算元或函式


增量運算


型別轉換

轉載宣告:本文轉載自「Python開發者」,搜尋「PythonCoder」即可關註。

《Python人工智慧和全棧開發》2018年07月23日即將在北京開課,120天衝擊Python年薪30萬,改變速約~~~~

*宣告:推送內容及圖片來源於網路,部分內容會有所改動,版權歸原作者所有,如來源資訊有誤或侵犯權益,請聯絡我們刪除或授權事宜。


– END –


更多Python好文請點選【閱讀原文】哦

↓↓↓

贊(0)

分享創造快樂