Python索引和迭代

前言

本文內容主要是對 Learn Python 索引迭代部分內容的摘抄。書籍內容講的非常詳細,完整閱讀請看原文。

重載運算符

下面從Learn Python摘抄的部分重載運算符,所有重載方法前後都有兩個下劃線,以便把同類中定義的變量名區分開來。特殊方法名稱和表達式或運算的映射關係,是由Python提前定義好的。例如,名稱 __add__ 方法的代碼實際在做什麼,總是對應到了表達式 +

方法 重載 調用
__getitem__ 索引運算 X[key], X[i:j], 沒 __iter__ 的for循環和其它迭代器
__setitem__ 索引賦值運算 X[key]=value, X[i:j]=sequence
__iter__, __next__ 迭代環境 I=iter(X), next(I); for loops, in if no *\_\_contains\_\_*allcomprehensions, map(F, X), 其它(__next__)在Python2.6稱爲next
__contains__ 成員關係測試 item in X(任何可迭代的)
__index__ 整數值 hex(X), bin(X), oct(X), O[X], O[X:](替代Python2中的 __oct____hex__

索引和分片,getitem__和__setitem

如果在類中定義或繼承了 __getitem__,則對於實例的索引運算,會自動調用 __getitem__。實例X出現在X[i]這樣的實例運算中時,Python會調用這個實例集成的 __getitem__ 方法,X作爲第一個參數傳遞,第二個參數位方括號內的索引值
self 就是實例X。

class Indexer:
    def __getitem__(self, item):
        return item ** 2

X = Indexer()
print(X[2])
for i in range[5]:
    print(i)

運行結果如下:

0 1 4 9 16

###攔截分片

除了索引,分片也使用 getitem。 正式的講,內置類型以同樣的方式處理分片。

L = [5, 6, 7, 8, 9]
print(L[2:4])
print(L[1:])
print(L[::2])

運行結果如下:

[7, 8]
[6, 7, 8, 9]
[5, 7, 9]

實際上,分片邊界綁定到了一個分片對象上,並且傳遞給索引的列表實現。實際上,我們總是可以手動的傳遞一個分片對象–分片語法主要是用一個分片對象進行索引的語法糖。下面介紹分片對象 slice

L = [5, 6, 7, 8, 9]
print(L[slice(2, 4)])
print(L[slice(1, None)])
print(L[slice(None, -1)])
print(L[slice(None, None, 2)])

運行結果如下:

[7, 8]
[6, 7, 8, 9]
[5, 6, 7, 8]
[5, 7, 9]

在此,我們可以猜測分片語法 start:stop:step, 產生了slice對象,並傳遞給列表。

對於一個帶有 __getitem__ 的類,重要的事實是該方法既針對基本索引調用,又針對分片調用。Indexr的 __getitem__ 可以同時處理整數索引和分片對象 slice。這也驗證了我們上面的推測,即分片語法產生分片對象 slice

class Indexr:
    data = [5, 6, 7, 8, 9]

    def __getitem__(self, index):
        print('getitem:', index)
        return self.data[index]

x = Indexr()
print(x[0])
print(x[1:3])

運行結果:

getitem: 0
5
getitem: slice(1, 3, None)
[6, 7]

如果使用的話, *__setitem__*索引賦值方法類似的攔截索引和分片賦值—它爲後者接收了一個分片對象,它可能以同樣的方式傳遞到類似的另一個索引賦值中。value可能是一個整數值也可能是個索引對象。

def __setitem__(self, index, value):
    self.data[index] = value

Python2.6中的分片和索引

在Python3之前,類也可以定義 __getslice____setslice__ 方法來專門攔截分片獲取和賦值。它們將傳遞一系列的分片表達式,並且優先於 __getitem__ 和 *__setitem__*用於分片。但是前兩種方法在Python3中被移除了,所以使用Python3不需要考慮這兩種方法。

不要混淆Python3中用於索引攔截的 __index__ 方法,該方法針對一個實例返回一個整數值,供轉換爲數字字符串的內置函數使用。

    class C:
        def __index__(self):
            return 255

    X = C()
    print(hex(X))
    print(bin(X))
    print(oct(X))

輸出結果:

0xff
0b11111111
0o377

該方法在Python2.6以同樣的方式工作,只不過他不會針對 hexoct 內置函數調用(在python2.6中使用 __hex____oct__ 來攔截這些調用)。

讓人困惑的 index

__index__ 來自官方的PEP357解釋了這一行爲。以下爲翻譯內容:

NumPy開發人員遇到了一個問題,只能通過添加一個新的特殊方法 __index__ 來解決。當使用如 [start:stop:step] 的分片表示法時,start、stop和step 只能使用整數或長整數。NumPy定義了與 8,16,32和64 位的無符號和有符號整數相對應的專用整數類型,但是沒有辦法指示這些類型可以用作索引切片。

切片不能使用現有的 __int__ 方法, 因爲該方法也用於對整數的強制轉換。如果切片使用 int, 浮點數也會成爲合法的切片索引,這顯然是一種不良行爲。相反,一個名爲 __index__ 的新特殊方法被添加了,它不帶參數,並返回一個整數,給出要使用的切片索引。 __index__ 返回值必須是Python整數或長整數。解釋器將檢查返回的類型是否正確,不滿組條件,則會引發TypeError.

class C:
    def __index__(self, value):
        return 255 

總結,__index__ 的作用是爲了Numpy的自定義整數類型,能夠像Python內置整數一樣表示分片對象。

索引迭代 getitem

for 語句的作用是從0到更大的索引值,重複對序列進行索引運算,直到檢測到超出邊界的異常。因此, __getitem__ 也可以是Pyhton中一種重載索引迭代的方式。如果定義了這個方法,for 循環每次循環時都會調用類的 __getitem__,並持續搭配有更高的索引值。這是一種“買一送一”的情況,任何會響應索引迭代的內置或用戶自定義對象,同樣會響應迭代。

class stepper:
    def __init__(self, data):
        self.data = data

    def __getitem__(self, i):
        return self.data[i]

x = stepper('spam')
print(x[1])
for item in x:
    print(item)

運行結果:

p
s
p
a
m

任何支持 for 循環迭代的類也會自動支持Python所有的迭代環境。如,成員關係測試 in,列表解析,內置函數 map,列表和元組賦值以及類型構造方法也會調用 __getitem__ (如果定義了的話)。

迭代器對象 iternext

儘管 __getitem__ 技術有效, 但它真的只是迭代的一種退而求其次的方法。如今 Python所有的迭代環境都會先嚐試 __iter__ 方法,再嘗試 __getitem__ 。 只有在對象不支持迭代協議的時候纔會嘗試索引運算。 一般來講,你應該優先使用 __iter__ ,它能夠比 __getitem__ 更好地支持一般的迭代環境。

從技術角度來講,迭代環境是通過調用內置函數 iter 去嘗試尋找 __iter__ 方法來實現的,而這種方法應該返回一個迭代器對象。 如果提供了,Python就會重複調用這個迭代器對象的 next 方法,直到發生 StopIteration 異常。 如果沒找到這類 __iter__ 方法, Pyhton就會改用 __getitem__ 就像之前那樣通關過偏移量重複索引,直到引起 IndexError

class stepper:
    def __init__(self, data):
        self.data = data
        self.index = 0

    def __getitem__(self, i):
        print('getitem', end=' ')
        return self.data[i]

    def __iter__(self):
        print('iter', end='\r\n')
        return self

    def __next__(self):
        print('next', end=' ')
        if self.index == len(self.data):
            print('raise StopIteration!')
            raise StopIteration
        else:
            self.index += 1
        return self.data[self.index - 1]

x = stepper('spam')
for item in x:
    print(item)

運行結果:

iter
next s
next p
next a
next m
next raise StopIteration!

先不討論 stepper 的實現細節,它同時存在 __getitem__ 和 *__iter__*方法。在 for 的迭代環境中,stepper忽略了索引運算,而是優先使用 __iter__ 方法,並由 __iter__ 調用 __next__ 內置函數,直到引發 StopIteration

用戶定義的迭代器

__iter__ 機制中, 類就是通過實現第14章和第20章介紹的迭代協議(見書本原文),來實現用戶定義的迭代器的。 例如,下面的文件 iters.py, 定義了用戶定義的迭代器類來生成平方值。

# iters.py

class Squares:
    def __init__(self, start, stop):
        self.value = start - 1
        self.stop = stop

    def __iter__(self):
        return self

    def __next__(self):
        if self.value == self.stop:
            raise StopIteration
        self.value += 1
        return self.value **2

for i in Squares(1, 5):
    print(i, end=' ')

# run result
>>> 1 4 9 16 25

在這裏, 迭代器就是實例 self, 因爲 next 方法是這個類的一部分。在較爲複雜的場景中,迭代器對象可定義爲個別的類或有自己狀態信息的對象, 對相同對象支持多種迭代(下面會看到這種例子),以Python raise 語句發出信號表示迭代結束。 手動迭代對內置類型也有效(主動調用next方法)

>>> X = Squares(1, 3)
>>> I = iter(X)
>>> print(next(I))
>>> print(I.__next__())
>>> print(next(I))
>>> print(next(I)) 
StopIteration

__getitem__ 所寫的等效代碼可能不是很自然,因爲 for 會對所有的0和較高值的偏移值進行迭代, 傳入的偏移值和所產生的值的範圍只有間接的關係(0…N 需要映射爲 start…stop)。 因爲 __iter__ 對象會在調用過程中明確的保留狀態信息,所以比 __getitem__ 具有更好的通用性。 這裏還需要多理解理解

__getitem__ 不同的是 __iter__ 只循環一次,而不是循環多次。例如 Square類只循環一次,循環之後就變爲空。每次新的循環,都得創建一個新的迭代器對象。

X = Squares(1, 3)
print([n for n in X])
print([n for n in X])
X = Squares(1, 5)
print([n for n in X])

# run result
[1, 4, 9]
[]
[1, 4, 9, 16, 25]

有多個迭代器的對象

對S來講, 外層循環調用 iter 從對象中獲取迭代器,而每個嵌套的循環也做相同的事來獲得獨立的迭代器。 因爲每個激活狀態下的迭代器都有自己的狀態信息,而不管其它激活狀態下的循環是什麼狀態。 X返回的迭代器是自身, 嵌套的循環迭代同一個迭代器, 所以外訓環加內循環達到三次,結束迭代。

X = Squares(1, 3)
for x in X:
    for y in X:
        print('%d * %d' % (x, y), end=' ')

S = 'abc'
for x in S:
    for y in S:
        print(x + y, end=' ')

# run result

1 * 4 1 * 9
aa ab ac ba bb bc ca cb cc 

當我們用類編寫用戶定義的迭代器的時候,由我們決定是否支持一個的或是多個活躍的迭代。 要達到多個迭代的效果, iter 只需替迭代器定義新的狀態對象,而不是返回self。

例如, 下面定義了一個迭代器類,迭代時,跳過下一個元素。 因爲迭代器對象會在每次迭代時都重新創建,所以能夠支持多個處於激活狀態下的循環。

class SkipIterator:
    def __init__(self, wrapped):
        self.wrapped = wrapped
        self.offest = 0

    def __next__(self):
        if self.offest >= len(self.wrapped):
            raise StopIteration
        else:
            item = self.wrapped[self.offest]
            self.offest += 2
            return item

class SkipObject:
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def __iter__(self):
        # return a new iterator 
        return SkipIterator(self.wrapped)

if __name__ == '__main__':
    alpha = 'abcdef'
    skipper = SkipObject(alpha)
    I = iter(skipper)
    print(next(I), next(I), next(I))
    for x in skipper:
        for y in skipper:
            print(x + y, end=' ')

運行時,這個例子工作起來就像是對內置字符串進行嵌套循環一樣, 因爲每個循環都會獲得獨立的迭代器對象來記錄自己的狀態信息, 所以每個激活狀態下的循環都有自己再字符串中的位置。

a c e
aa ac ae ca cc ce ea ec ee 

成員關係: containsiter__和__getitem

在迭代領域,類通常把 in 成員關係運算符實現爲一個迭代, 使用 __iter__ 方法或 __getitem__ 方法。 要支持更加特定的成員關係,類可能編寫一個 __contains__ 方法——當出現時,該方法優先於 __iter__ 方法, __iter__ 方法優先於 __getitem__ 方法。 __contains__ 方法應該把成員關係定義爲一個映射應用鍵,以及用於序列的搜索。

總結:優先級順序,__contains__ > __iter__ > __getitem__ (getitem無人權!)

考慮如下的類, 它編寫了以上3個方法和測試成員關係,以及應用於一個實例的各種迭代環境。調用的時候,其方法會打印出跟蹤消息:

class Iters:
    def __init__(self, value):
        self.data = value

    def __getitem__(self, i):
        print('get[%s]:' % i, end='')
        return self.data[i]

    def __iter__(self):
        print('iter=>', end='')
        self.ix = 0
        return self

    def __next__(self):
        print('next:', end='')
        if self.ix == len(self.data):
            raise StopIteration
        item = self.data[self.ix]
        self.ix += 1
        return item

    def __contains__(self, item):
        print('contains: ', end='')
        return item in self.data

    def __index__(self):
        return 255

if __name__ == '__main__':
    X = Iters([1, 2, 3, 4, 5])
    print(3 in X)
    for i in X:
        print(i, end=' | ')

    print()
    print([i ** 2 for i in X])
    print(list(map(bin, X)))

    I = iter(X)
    while True:
        try:
            print(next(I), end=' @ ')
        except StopIteration:
            break

這段腳本運行的時候, 其輸出如下所示——特定的 __contains__ 攔截成員關係,通用的 __iter__ 捕獲其它迭代環境以至 __next__ 重複的被調用, 而 _getitem_ 不會被調用。

contains: True
iter=>next:1 | next:2 | next:3 | next:4 | next:5 | next:
iter=>next:next:next:next:next:next:[1, 4, 9, 16, 25]
iter=>next:next:next:next:next:next:['0b1', '0b10', '0b11', '0b100', '0b101']
iter=>next:1 @ next:2 @ next:3 @ next:4 @ next:5 @ next:

但是, 要觀察如果註釋掉 __contains__ 方法後代碼的輸出發生了什麼變化——成員關係現在路由到了通用的 __iter__

iter=>next:next:next:True
iter=>next:1 | next:2 | next:3 | next:4 | next:5 | next:
iter=>next:next:next:next:next:next:[1, 4, 9, 16, 25]
iter=>next:next:next:next:next:next:['0b1', '0b10', '0b11', '0b100', '0b101']
iter=>next:1 @ next:2 @ next:3 @ next:4 @ next:5 @ next:

最後,如果 __contains____iter__ 都註釋掉的話,其輸出如下——索引 __getitem__ 替代方法會被調用,針對成員關係和其它迭代環境使用持續增加的索引。

get[0]:get[1]:get[2]:True
get[0]:1 | get[1]:2 | get[2]:3 | get[3]:4 | get[4]:5 | get[5]:
get[0]:get[1]:get[2]:get[3]:get[4]:get[5]:[1, 4, 9, 16, 25]
get[0]:get[1]:get[2]:get[3]:get[4]:get[5]:['0b1', '0b10', '0b11', '0b100', '0b101']
get[0]:1 @ get[1]:2 @ get[2]:3 @ get[3]:4 @ get[4]:5 @ get[5]:

正如我們看到的, __getitem__ 方法甚至更加通用: 除了迭代,它還攔截顯式索引和分片。 分片表達式用包含邊界的一個分片對象來觸發 __getitem__, 既針對內置類型,也針對用戶定義的類,因此,我們的類中分片式自動化的。

X = Iters('spam')
print(X[0])
print(X[1:])
print(X[slice(None, -1)])

# run result
get[0]:s
get[slice(1, None, None)]:pam
get[slice(None, -1, None)]:spa

然而, 在並非面向序列的,更加現實的迭代用例中, __iter__ 方法可能更容易編寫,因爲它不必管理一個整數索引,並且 __contains__ 可以作爲一種特殊情況優化成員關係。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章