在使用Python的過程中,很容易混淆如下幾個關聯的概念:
- 容器(container)
- 可迭代物件(Iterable)
- 迭代器(Iterator)
- 生成器(generator)
- 生成器表示式
- {list, set, dict} 解析式
它們之間的關係如下表所示:
容器(container)
容器是用來儲存元素的一種資料結構,它支援隸屬測試,容器將所有資料儲存在記憶體中,在Python中典型的容器有:
- list, deque, …
- set,frozesets,…
- dict, defaultdict, OrderedDict, Counter, …
- tuple, namedtuple, …
- str
容器相對來說很好理解,因為你可以把它當成生活中的箱子、房子、船等等。
一般的,通過判斷一個物件是否包含某個元素來確定它是否為一個容器。例如:
1 2 3 4 5 6 |
>>> assert 1 in [1,2,3] # lists >>> assert 4 not in [1,2,3] >>> assert 1 in {1,2,3} # sets >>> assert 4 not in {1,2,3} >>> assert 1 in (12,3) # tuples >>> assert 4 not in (1,2,3) |
字典容器通過檢查是否包含鍵來進行判斷:
1 2 3 4 |
>>> d = {1:"foo", 2:"bar", 3:"qux"} >>> assert 1 in d >>> assert 4 not in d >>> assert "foo" not in d |
字串通過檢查是否包含某個子 串來判斷:
1 2 3 4 |
>>> s ="foobar" >>> assert "b" in s >>> assert "x" not in s >>> assert "foo" in s |
注意:並非所有的容器都是可迭代物件。
可迭代物件
正如前面所提到的,大部分容器都是可迭代的,但是還有其他一些物件也可以迭代,例如,檔案物件以及管道物件等等,容器一般來說儲存的元素是有限的,同樣的,可迭代物件也可以用來表示一個包含有限元素的資料結構。
可迭代物件可以為任意物件,不一定非得是基本資料結構,只要這個物件可以返回一個iterator。聽起來可能有點費解,但是可迭代物件與迭代器之間有一個顯著的區別。先看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> x = [1,2,3] >>> y = iter(x) >>> z = iter(x) >>> next(y) 1 >>> next(y) 2 >>> next(z) 1 >>> type(x) <class 'list'> >>> type(y) <class 'list_iterator'> |
在這裡,x是可迭代物件,而y和z都是迭代器,它們從可迭代物件x中獲取值。
注意:可迭代的類中,一般實現以下兩個方法,
__iter__()
以及__next()__
方法,__iter__()
方法返回self。
當我們執行以下程式碼的時候:
1 2 3 |
x = [1,2,3] for elem in x: ... |
實際呼叫過程如下:
當我們反向編譯這段代Python碼的時候,可以發現它顯示呼叫了 GET_ITER
,本質上跟呼叫iter(x)
一樣,而FOR_ITER
指令相等於呼叫next()
方法來獲取每個元素。
1 2 3 4 5 6 7 8 9 10 11 12 |
>>> import dis >>> x = [1, 2, 3] >>> dis.dis('for _ in x: pass') 1 0 SETUP_LOOP 14 (to 17) 3 LOAD_NAME 0 (x) 6 GET_ITER >> 7 FOR_ITER 6 (to 16) 10 STORE_NAME 1 (_) 13 JUMP_ABSOLUTE 7 >> 16 POP_BLOCK >> 17 LOAD_CONST 0 (None) 20 RETURN_VALUE |
迭代器(Iterators)
那麼什麼是迭代器呢?任何具有__next__()
方法的物件都是迭代器,對迭代器呼叫next()方法可以獲取下一個值。而至於它使如何產生這個值的,跟它能否成為一個迭代器並沒有關係。
所以迭代器本質上是一個產生值的工廠,每次向迭代器請求下一個值,迭代器都會進行計算出相應的值並返回。
迭代器的例子很多,例如,所有itertools
模組中的函式都會返回一個迭代器,有的還可以產生無窮的序列。
1 2 3 4 5 6 |
>>> from itertools import count >>> counter = count(start=13) >>> next(counter) 13 >>> next(counter) 14 |
有的函式根據有限序列中生成無限序列:
1 2 3 4 5 6 7 8 9 10 |
>>> from itertools import cycle >>> colors = cycle(["red","white","blue"]) >>> next(colors) "red" >>> next(colors) "white" >>> next(colors) "blue" >>> next(colors) "red" |
有的函式根據無限序列中生成有限序列:
1 2 3 4 5 6 7 8 9 |
>>> from itertools import islice >>> colors = cycle(['red', 'white', 'blue']) # infinite >>> limited = islice(colors, 0, 4) # finite >>> for x in limited: # so safe to use for-loop on ... print(x) red white blue red |
為了更好的理解迭代器的內部結構,我們先來定義一個生成斐波拉契數的迭代器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
>>> class fib: ... def __init__(self): ... self.prev = 0 ... self.curr = 1 ... ... def __iter__(self): ... return self ... ... def __next__(self): ... value = self.curr ... self.curr += self.prev ... self.prev = value ... return value ... >>> f = fib() >>> list(islice(f, 0, 10)) [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] |
注意這個類既是可迭代的 (因為具有__iter__()
方法),也是它自身的迭代器(因為具有__next__()
方法)。
迭代器內部狀態儲存在當前例項物件的prev
以及cur
屬性中,在下一次呼叫中將使用這兩個屬性。每次呼叫next()
方法都會執行以下兩步操作:
- 修改狀態,以便下次呼叫
next()
方法 - 計算當前呼叫的結果
比喻:從外部來看,迭代器就像政府工作人員一樣,沒人找他辦事的時候(請求值),工作人員就閒著,當有人來找他的時候(請求值),工作人員就會忙一會,把請求的東西找出來交給請求的人。忙完之後,又沒事了,繼續閒著。
生成器
生成器其實就是一種特殊的迭代器。它使一種更為高階、更為優雅的迭代器。
使用生成器讓我們可以以一種更加簡潔的語法來定義迭代器。
讓我們先明確以下兩點:
- 任意生成器都是迭代器(反過來不成立)
- 任意生成器,都是一個可以延遲建立值的工廠
下面也是一個生成斐波那契序列的工廠函式,不過是以生成器的方式編寫的:
1 2 3 4 5 6 7 8 9 |
>>> def fib(): ... prev, curr = 0, 1 ... while True: ... yield curr ... prev, curr = curr, prev + curr ... >>> f = fib() >>> list(islice(f, 0, 10)) [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] |
上面的程式碼是不是既優雅又簡潔?注意其中用到的魔法關鍵字
1 |
yield |
一起來剖析下上面的程式碼:首先,fib其實是一個很普通的函式,但是函式中沒有return
語句,函式的返回值是一個生成器。
當呼叫f = fib()
時,生成器被例項化並返回,這時並不會執行任何程式碼,生成器處於空閒狀態,注意這裡prev, curr = 0, 1
並未執行。
然後這個生成器被包含在isslice()
中,而這又是一個迭代器,所以還是沒有執行上面的程式碼。
然後這個迭代器又被包含在list()
中,它會根據傳進來的引數生成一個列表。所以它首先對isslice()
物件呼叫next()
方法,isslice()
物件又會對例項f
呼叫next()
。
我們來看其中的一步操作,在第一次呼叫中,會執行prev, curr = 0, 1
, 然後進入while迴圈,當遇到yield curr
的時候,返回當前curr值,然後又進入空閒狀態。
生成的值傳遞給外層的isslice()
,也相應生成一個值,然後傳遞給外層的list()
,外層的list將這個值1新增到列表中。
然後繼續執行後面的九步操作,每步操作的流程都是一樣的。
然後執行到底11步的時候,isslice()
物件就會丟擲StopIteration
異常,意味著已經到達末尾了。注意生成器不會接收到第11次next()
請求,後面會被垃圾回收掉。
生成器的型別
在Python中兩種型別的生成器:生成器函式以及生成器表示式。生成器函式就是包含yield
引數的函式。生成器表示式與列表解析式類似。
假設使用如下語法建立一個列表:
1 2 3 |
>>> numbers = [1, 2, 3, 4, 5, 6] >>> [x * x for x in numbers] [1, 4, 9, 16, 25, 36] |
使用set解析式也可以達到同樣的目的:
1 |
>>> {x * x for x in numbers}{1, 4, 36, 9, 16, 25} |
或者dict解析式:
1 2 |
>>> {x: x * x for x in numbers} {1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36} |
還可以使用生成器表示式:
1 2 3 4 5 6 7 |
>>> lazy_squares = (x * x for x in numbers) >>> lazy_squares <generator object <genexpr> at 0x10d1f5510> >>> next(lazy_squares) 1 >>> list(lazy_squares) [4, 9, 16, 25, 36] |
注意我們第一次呼叫next()
之後,lazy_squares
物件的狀態已經發生改變,所以後面後面地呼叫list()
方法只會返回部分元素組成的列表。
總結
生成器是Python中一種非常強大的特性,它讓我們能夠編寫更加簡潔的程式碼,同時也更加節省記憶體,使用CPU也更加高效。
使用生成器的小提示:在你的程式碼中找到與下面程式碼類似的地方:
1 2 3 4 5 |
def something(): result = [] for ... in ...: result.append(x) return result |
用以下程式碼進行替換:
1 2 3 |
def iter_something(): for ... in ...: yield x |