接下來三天,我們介紹Python三種常用的protocols。
根據Python docs的說明,sequence是一個實作有__len__及__getitem__,且能以整數作為index取值的iterable。
Sequence protocol有時候也被稱作old-style iteration protocol。
__len____len__必須回傳整數值。它除了讓我們可以使用len(obj)的語法來得知sequence的長度外,也會在沒有實作某些dunder method時,和__getitem__聯手,提供相同的功能。
__getitem____getitem__是一個有趣的dunder method,它讓我們能夠使用[]來取值,像list因此可以使用整數或slice作為index來取值,而dict因此可以使用hashable的obj作為key來取值。
__iter__的備案當__getitem__符合下列條件時,sequence可以在不實作__iter__的情況下,視為iterable。
index會從0開始呼叫,當index在可取值範圍內回傳其值。當超出範圍時,raise IndexError。
這個描述非常類似list,而的確我們也常使用list作為sequence所真正包含的容器。
__contains__的備案__contains__是Python用來處理membership test的dunder method。當使用in obj,而obj沒有實作__contains__時,會改使用__iter__。如果再沒有實作__iter__的話,會改使用__getitem__。
__reversed__的備案__reversed__一般被Python的built-in reversed所呼叫。當使用reversed(obj),而obj沒有實作__reversed__時,會使用__getitem___和__len__來達成reversed的功能。
# 01中MySeq是一個實作有__getitem__與__len__的class,所以my_seq可以使用[]來取值。
# 01
class MySeq:
def __init__(self, iterable):
self._list = list(iterable)
def __len__(self):
print('__len__ called')
return len(self._list)
def __getitem__(self, value):
print(f'__getitem__ called, {value=}')
try:
return self._list[value]
except Exception as e:
print(type(e), e)
raise
if __name__ == '__main__':
my_seq = MySeq(range(3))
print('*****test []*****')
print(f'{my_seq[0]=}') # 0
*****test []*****
__getitem__ called, value=0
my_seq[0]=0
由於我們將__getitem__內取值的任務delegate給list,所以符合前面「__iter__的備案」所述的條件,因此Python會將my_seq視為iterable。
# 01
...
if __name__ == '__main__':
...
print('*****test is an iterable*****')
for item in my_seq:
pass
*****test is an iterable*****
__getitem__ called, value=0
__getitem__ called, value=1
__getitem__ called, value=2
__getitem__ called, value=3
<class 'IndexError'> list index out of rang
我們也可以觀察,當index=3時,因為超過了my_seq能接收的範圍,self._list會報錯,而其錯誤型態的確為IndexError。
# 01
...
if __name__ == '__main__':
...
print('*****test in operator*****')
print(f'{2 in my_seq=}')
*****test in operator*****
__getitem__ called, value=0
__getitem__ called, value=1
__getitem__ called, value=2
2 in my_seq=True
由於my_seq沒有實作__contains__與__iter__,所以Python會依靠__getitem__逐個取值,來比對2有沒有在my_seq中。
# 01
...
if __name__ == '__main__':
...
print('*****test is reversible*****')
for i in reversed(my_seq):
pass
*****test is reversible*****
__len__ called
__getitem__ called, value=2
__getitem__ called, value=1
__getitem__ called, value=0
由於my_seq沒有實作__reversed__,所以Python會同時使用__getitem__及__len__來達成reversed的功能。
Python的collections.abc中有Sequence及MutableSequence兩種abstract base class,方便我們繼承使用。文件中有說明我們必須實作哪些dunder method,而根據這些dunder method,Python將能自動提供其它額外的method可以使用。
如果繼承Sequence的話,只需要實作__getitem__與__len__,就能額外獲得__contains__、__iter__、__reversed__、index與count。
如果繼承MutableSequence的話,只需要實作__getitem__、 __setitem__、__delitem__、__len__與insert,就能獲得繼承Sequence額外獲得的method加上append、reverse、extend、pop、remove與 __iadd__。
雖然只實作__getitem__與__len__,就可以作為很多dunder method的備案,但是依靠__getitem__逐個取值的效率是比較差的。所以如果可能的話,我們會建議針對各種dunder method實作比較有效率的邏輯。