自定義序列的相關魔法方法允許我們自己創建的類擁有序列的特性,讓其使用起來就像 python 的內置序列(dict,tuple,list,string等)。
如果要實現這個功能,就要遵循 python 的相關的協議。所謂的協議就是一些約定內容。例如,如果要將一個類要實現迭代,可以實現__iter__() 或者 __getitem__()其中一個方法。
下面是一下相關的魔法方法:
-
__len__(self)
-
返回容器的長度。可變和不可變容器都要實現它,這是協議的一部分。
- __getitem__(self, key)
-
定義當某一項被訪問時,使用self[key]所產生的行為。這也是可變容器和不可變容器協議的一部分。如果鍵的類型錯誤將產生TypeError;如果key沒有合適的值則產生KeyError。
-
__setitem__(self, key, value)
-
定義當一個條目被賦值時,使用self[key] = value所產生的行為。這也是可變容器協議的一部分。而且,在相應的情形下也會產生KeyError和TypeError。
-
__delitem__(self, key)
-
定義當某一項被刪除時所產生的行為。(例如del self[key])。這是可變容器協議的一部分。當你使用一個無效的鍵時必須拋出適當的異常。
-
__iter__(self)
-
返回一個迭代器,尤其是當內置的iter()方法被調用的時候,以及當使用for x in container:方式進行循環的時候。
-
迭代器要求實現next方法(python3.x中改為__next__),並且每次調用這個next方法的時候都能獲得下一個元素,元素用盡時觸發 StopIteration 異常。
-
而其實 for 循環的本質就是先調用對象的__iter__方法,再不斷重復調用__iter__方法返回的對象的 next 方法,觸發 StopIteration 異常時停止,並內部處理了這個異常,所以我們看不到異常的拋出。
這種關系就好像接口一樣,如果回顧以前幾篇的魔法方法,可以發現許多的內置函數得到的結果就是相應的魔法方法的返回值。
- 可迭代對象:對象實現了一個__iter__方法,這個方法負責返回一個迭代器。
- 迭代器:內部實現了next(python3.x為__next__)方法,真正負責迭代的實現。當迭代器內的元素用盡之后,任何的進一步調用都之后觸發 StopIteration 異常,所以迭代器需要一個__iter__方
- 法來返回自身。所以大多數的迭代器本身就是可迭代對象。這使兩者的差距進一步減少。
- 但是兩者還是不同的,如果一個函數要求一個可迭代對象(iterable),而你傳的迭代器(iterator)並沒有實現__iter__方法,那么可能會出現錯誤。
- 不過一般會在一個類里同時實現這兩種方法(即是可迭代對象又是迭代器),此時__iter__方法只要返回self就足夠的了。當然也可以返回其它迭代器。
-
__reversed__(self)
-
實現當reversed()被調用時的行為。應該返回序列反轉后的版本。僅當序列是有序的時候實現它,例如列表或者元組。
-
__contains__(self, item)
-
定義了調用in和not in來測試成員是否存在的時候所產生的行為。這個不是協議要求的內容,但是你可以根據自己的要求實現它。當__contains__沒有被定義的時候,Python會迭代這個序列,並且當找到需要的值時會返回True。
-
__missing__(self, key)
-
其在dict的子類中被使用。它定義了當一個不存在字典中的鍵被訪問時所產生的行為。(例如,如果我有一個字典d,當"george"不是字典中的key時,使用了d["george"],此時d.__missing__("george")將會被調用)。
下面是一個代碼示例:
class Foo(object): def __init__(self, key, value): self.key = [] self.value = [] self.key.append(key) self.value.append(value)
self.__index = 0 def __len__(self): return len(self.key) def __getitem__(self, item): try: __index = self.key.index(item) return self.value[__index] except ValueError: raise KeyError('can not find the key') def __setitem__(self, key, value): if key not in self.key: self.key.append(key) self.value.append(value) else: __index = self.key.index(key) self.value[__index] = value def __delitem__(self, key): try: __index = self.key.index(key) del self.key[__index] del self.value[__index] except ValueError: raise KeyError('can not find the key') def __str__(self): result_list = [] for index in xrange(len(self.key)): __key = self.key[index] __value = self.value[index] result = __key, __value result_list.append(result) return str(result_list) def __iter__(self):return self def next(self): if self.__index == len(self.key): self.__index = 0 raise StopIteration() else: __key = self.key[self.__index] __value = self.value[self.__index] result = __key, __value self.__index += 1 return result def __reversed__(self): __result = self.value[:] __result.reverse() return __result def __contains__(self, item): if item in self.value: return True else: return False
這里創建一個模擬字典的類,這個類的內部維護了兩個列表,key 負責儲存鍵,value 負責儲存值,兩個列表通過索引的一一對應,從而達到模擬字典的目的。
首先,我們看看__len__方法,按照協議,這個方法應該返回容器的長度,因為這個類在設計的時候要求兩個列表必須等長,所以理論上返回哪個列表的長度都是一樣的,這里我選擇返回 key 的長度。
然后是__getitem__方法。這個方法會在a['scolia']時,調用a.__getitem__('scolia')。也就是說這個方法定義了元素的獲取,我這里的思路是先找到 key 列表中建的索引,然后用索引去 value 列表中找對應的元素,然后將其返回。然后為了進一步偽裝成字典,我捕獲了可能產生的 ValueError (這是 item 不在 key 列表中時觸發的異常),並將其偽裝成字典找不到鍵時的 KeyError。
理論上只要實現了上面兩個方法,就可以得到一個不可變的容器了。但是我覺得並不滿意所以繼續拓展。
__setitem__(self, key, value)方法定義了 a['scolia'] = 'good' 這種操作時的行為,此時將會調用a.__setitem__('scolia', 'good') 因為是綁定方法,所以self是自動傳遞的,我們不用理。這里我也模擬了字典中對同一個鍵賦值時會造成覆蓋的特性。這個方法不用返回任何值,所以return語句也省略了。
__delitem__(self, key)方法定義了del a['scolia'] 這類操作時候的行為,里面的‘scolia’就作為參數傳進去。這里也進行了異常的轉換。
只有實現里以上四個方法,就可以當做可變容器來使用了。
接下來的 __str__ 是對應於 str() 函數,在類的表示中會繼續討論,這里是為了 print 語句好看才加進去的,因為print語句默認就是調用str()函數。
__iter__和next方法在開頭的時候討論過了,這里是為了能讓其進行迭代操作而加入的。
__reversed__(self)方法返回一個倒序后的副本,這里體現了有序性,當然是否需要還是要看個人。
__contains__實現了成員判斷,這里我們更關心value列表中的數據,所以判斷的是value列表。該方法要求返回布爾值。
下面是相應的測試:
a = Foo('scolia', 'good') a[123] = 321 a[456] = 654 a[789] = 987 print a del a[789] print a for x, y in a: print x, y print reversed(a) print 123 in a print 321 in a

-
__missing__(self, key)
class Boo(dict): def __new__(cls, *args, **kwargs): return super(Boo, cls).__new__(cls) def __missing__(self, key): return 'The key(%s) can not be find.'% key
測試:
b = Boo() b['scolia'] = 'good' print b['scolia'] print b['123']

當然你也可以在找不到 key 的時候觸發異常,具體實現看個人需求。
只用__getitem__(self, item)實現支持for循環:
class Foo(object): def __init__(self, x): self.x = x self.__index = -1 def __getitem__(self, item): self.__index += 1 return self.x[self.__index]
測試:
a = Foo([1, 2, 3]) for x in a: print x

工作良好。
切片操作的實現:
有好奇的同學可能還會發現上面並沒有出現序列的典型操作:切片的實現。
其實切片也是使用__getitem__(self, item)魔法方法的,先讓我們看看當我們使用切片的時候,item參數會獲得什么:
class Foo(object): def __init__(self, x): self.x = x def __getitem__(self, item): return item a = Foo(123) print a[1:2]

獲得了一個類似函數的對象,其類型為:

該類型由 slice 函數創建,感興趣的同學可以使用 help 函數進行深入研究。
該函數的創建方法為: slice(stop)/slice(start, stop[, step]) 兩種,一旦創建后,我們可以使用 start、stop、step屬性來獲取相應的值。
如果要讓上面的例子支持切片,只需要修改__getitem__(self, item)處的代碼:
def __getitem__(self, item): if isinstance(item, slice): return self.value[item.start:item.stop:item.step] else: try: __index = self.key.index(item) return self.value[__index] except ValueError: raise KeyError('can not find the key')
輸出:
a = Foo('scolia', 'good') a[123] = 321 a[456] = 654 a[789] = 987 print a[:] print a[2:] print a[:3] print a[1:5] print a[1:10:2] print a[-4:-2]

運行良好,切片功能支持完畢。
歡迎大家交流。
參考資料:戳這里
