iT邦幫忙

2023 iThome 鐵人賽

DAY 21
0
Software Development

Python十翼:與未來的自己對話系列 第 21

[Day21] 七翼 - Protocols:Iteration Protocol

  • 分享至 

  • xImage
  •  

首先我們要先來聊聊iterableiterator

iterable vs iterator

iterablePython docs的說明:

... When an iterable object is passed as an argument to the built-in function iter(), it returns an iterator for the object. ...

我們可以定義說,如果將obj傳遞給iter,能夠順利取得iterator而不raise TypeError的話,那麼該obj就是iterable

iterator又該怎麼定義呢?從iteratorPython docs的說明:

... Repeated calls to the iterator’s __next__() method (or passing it to the built-in function next()) return successive items in the stream. When no more data are available a StopIteration exception is raised instead. At this point, the iterator object is exhausted and any further calls to its __next__() method just raise StopIteration again. Iterators are required to have an __iter__() method that returns the iterator object itself so every iterator is also iterable and may be used in most places where other iterables are accepted. ...

我們可以總結:

  • 能夠使用nextobj__next__連續取值。
  • 如果該iterator是有限的,那麼iterator耗盡後再呼叫nextobj__next__,會raise StopIteration
  • iterator是一種iterable。當對iterator使用iter時,將取回其iterator本身。

如果滿足以上幾點的話,那麼該obj就是iterator

iteration protocol

iteration protocol會先看看obj是否有實作__iter__,如果有的話就嘗試是否能透過呼叫__iter__得到iterator,並利用nextiterator__next__取值。如果沒有實作__iter__,卻滿足昨天的Sequence protocol時,會退一步使用__getitem__來取值。

如何生成iterator

如何生成iterator,我們提供以下五種方法。

方法1:利用iter來得到其它iterableiterator

我們可以直接使用iter(obj)來取得objiterator

方法2:generator expression

generator expression的寫法與list comprehensions幾乎一樣,只是將[]改成(),其回傳型態是generator,可以視為一種iterator

方法3:generator function by yield

function中使用yield關鍵字,使得function成為一個generator function,其回傳型態也是generator

方法4:generator function by yield from

function中使用yield from關鍵字,使得function成為一個generator function,其回傳型態也是generator

方法5:iterator class

iterator class即是參照iterator基本定義,建立一個class,並照其protocol實作__iter____next__

實例

情境說明

假設您在一間新創公司辛苦打拼數年,最終如願IPO,於是決定將部份股份賣出,買下幾部心儀已久的車款。身為Python高手的您,決定寫個Garage class來管理這些車子,且Garage所生成的instance必須是個iterable,可以逐個顯示當前車庫內的車子。

實作細節

  • 首先您建立了Garage class,裡面有一些基礎的功能來幫助管理。
# 01
from contextlib import suppress


class Garage:
    def __init__(self, cars=()):
        self._cars = list(cars)

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

    def __getitem__(self, index):
        return self._cars[index]

    def add_car(self, car):
        self._cars.append(car)

    def remove_car(self, car):
        with suppress(ValueError):
            self._cars.remove(car)    
  • 因為Garage符合sequence protocol,所以其生成的instance會是一個iterable。但身為Python高手的您知道,這樣是比較沒有效率的,於是您決定試著實作上述五種生成iterator的方式。

方法1

# 01
class Garage:
    ...
    def __iter__(self):
        """method 1"""
        return iter(self._cars)

self._carslist型態,所以我們可以直接透過iter取得listiterator回傳,其型態為list_iterator

方法2

# 01
class Garage:
    ...
    def __iter__(self):
        """method 2"""
        return (car for car in self._cars)

self._carsiterable,所以我們可以對其打個迴圈,使用generator expression產生一個generator

方法3

# 01
class Garage:
    ...
    def __iter__(self):
        for car in self._cars:
            yield car

self._carsiterable,所以我們可以對其打個迴圈,利用yield關鍵字產生一個generator

方法4

# 01
class Garage:
    ...
    def __iter__(self):
        """method 4"""
        yield from self._cars

self._carsiterable,所以我們可以利用yield from關鍵字產生一個generator

方法5

# 01
class Garage:
    ...
    def __iter__(self):
        """method 5"""
        return GarageIterator(self)
    
    
class GarageIterator:
    def __init__(self, garage_obj):
        self._garage_obj = garage_obj
        self._index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self._index >= len(self._garage_obj):
            raise StopIteration
        car = self._garage_obj[self._index]
        self._index += 1
        return car

方法5我們於__iter__中回傳GarageIteratorinstance,其接受一個參數self(即Garage所生成的instance)。

我們逐一比對GarageIterator所生成的instance,是否會符合我們對iterator的定義:

  • 可以使用obj__next__連續取值。
  • iterator為有限的,當耗盡後若再呼叫nextobj__next__,會raise StopIteration
  • __iter__會回傳self,即GarageIterator生成的instance

發現全都符合,所以GarageIterator class是一個iterator class

實際使用

方法1~5都能正常使用。

# 01
...
if __name__ == '__main__':
    garage = Garage(['Koenigsegg Regera', 'Ford Mustang', 'Tesla Model X'])
    for car in garage:
        print(car)
Koenigsegg Regera
Ford Mustang
Tesla Model X

且當我們使用add_car加入新車到車庫後,__iter__也很聰明的能夠反應現況。

# 01
...
if __name__ == '__main__':
    ... 
    garage.add_car('Peugeot 308')
    for car in garage:
        print(car)  # Peugeot 308 now in garage
Koenigsegg Regera
Ford Mustang
Tesla Model X
Peugeot 308

特別的方法5

方法1~4是我們一般常使用的方法。方法5雖然明顯麻煩不少,但是我們可以偷偷改變iterator的狀態。

# 01
...
if __name__ == '__main__':
    ... 
    garage_iter = iter(garage)
    print(next(garage_iter))  # Koenigsegg Regera
    print(next(garage_iter))  # Ford Mustang
    print(next(garage_iter))  # Tesla Model X
    print(next(garage_iter))  # Peugeot 308

    garage_iter._index = 0
    print(next(garage_iter))  # Koenigsegg Regera

上面的程式中,我們先使用iter(garage)GarageIterator生成的iterator拿在手上。接下來呼叫四次next,分別取得Koenigsegg RegeraFord MustangTesla Model XPeugeot 308。接著我們將garage_iter._index設為0,然後再次呼叫next,就又可以再次取得Koenigsegg Regera了。

除了可以改變iterator的狀態外,我們還可以在GarageIterator內加上其它attributefunction,這是方法1~4無法做到的。

當日筆記

當需要生成iterator時,我們應該優先使用方法1~4,因為這幾個方法都能快速生成iterator。但當我們需要在iteration過程中改變iterator狀態,或需要有特殊的attributefunction可以使用的話,就得依照iterator的基本定義,來實作像方法5iterator class

參考資料

Code

本日程式碼傳送門


上一篇
[Day20] 七翼 - Protocols:Sequence Protocol
下一篇
[Day22] 七翼 - Protocols:Context Manager Protocol
系列文
Python十翼:與未來的自己對話30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言