Python 手記(一):生成器與迭代器


Python 手記(一):生成器與迭代器

一、列表生成式
在引入生成器和迭代器之前,首先還得提起列表生成式。List Comprehensions,是Python內置的非常簡潔強大的用作創建list列表的生成式。
假設:list1=[1,2,3,4,5,6,7,8,9,10],要求將列表內每個元素的值乘以2,怎么做呢?

Way1:新建一個list

>>>
>>> list1=[1,2,3,4,5,6,7,8,9,10]
>>>
>>> list2=[]
>>> for i in list1:
... list2.append(i*2)
...
>>> print(list2)
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
>>>

way2:原list修改值

>>>
>>> list1=[1,2,3,4,5,6,7,8,9,10]
>>> for i,e in enumerate(list1): //取下標
... list1[i]*=2
...
>>> print(list1)
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

有沒有更簡潔的辦法呢? of course,List Comprehensions!

>>>
>>> list1=[1,2,3,4,5,6,7,8,9,10]
>>> list2=[i*2 for i in list1] //列表生成式:將循環主體、條件、范圍寫在一起並用[]修飾生成list
>>>print(list2)
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

還可以加上條件判斷,例如取出list1中能被2整除的元素,乘以2

>>> list1=[1,2,3,4,5,6,7,8,9,10]
>>>
>>> list2=[i*2 for i in list1 if i % 2 ==0]
>>>
>>> print(list2)
[4, 8, 12, 16, 20]

雙重循環加條件判斷:

>>> a=[1,2,3]
>>> b=['a','b','c']
>>> c=[str(i)+j for i in a for j in b if i >= 2]
>>> print(c)
['2a', '2b', '2c', '3a', '3b', '3c']

不僅是list,for循環甚至可以用多個變量來迭代dict的key和value:

>>> # _*_conding:utf-8 _*_
>>> dict={'sz':'深圳','gz':'廣州','fs':'佛山'}
>>>
>>> for k,v in dict.items():
... print(k,'=',v)
...
sz = 深圳
gz = 廣州
fs = 佛山

那么同樣,可以用生成式來將dict生成list

>>> # _*_conding:utf-8 _*_
>>> dict={'sz':'深圳','gz':'廣州','fs':'佛山'}
>>>list=[k+'='+v for k,v in dict.items()]
>>>print(list)
['sz=深圳', 'gz=廣州', 'fs=佛山']

二、生成器Generator
上方描述了列表生成式是一次性將所有元素都裝入生成的list中,讀取list時也是一次性全部讀取,list是python最常用的數據類型之一,當數據規模非常大時(百萬千萬級別),如果繼續將如此大規模的數據一次性讀取進內存中再進行下一步操作,會帶來很長的等待時間和資源消耗,而通常我們所要讀取的是列表中的某一部分元素而不是全部,由此引入了generator的概念。

Generator的工作方式是:惰性運算,只保存算法不保存值,邊循環邊計算,僅當需要進入下一次循環時,才計算出list中的下一個元素,而不必創建完整的list.

Generator在python里的創建方式中最簡單的方法為:將生成式的[]修改為()即可生成一個generator,它的調用方法為內置的next()方法(或.next),每next()一次則運算一次,且順序執行指針方向不可逆,list遍歷結束后,會報錯StopIteration:

>>> list1=[1,2,3,4,5,6,7,8,9,10]
>>> a=(i*2 for i in list1 if i % 2 ==0)
>>> a
<generator object <genexpr> at 0x0000000000D380F8>
>>> print(a)
<generator object <genexpr> at 0x0000000000D380F8>
>>> print(a.__next__())
4
>>> print(next(a))
8
>>> print(next(a))
12
>>> print(next(a))
16
>>> print(next(a))
20
>>> print(next(a))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

通常調用生成器,也不會用next()方法一次次地來調用,而是使用for循環,因為生成器也是可迭代對象:

>>> list1=[1,2,3,4,5,6,7,8,9,10]
>>> a=(i*2 for i in list1 if i % 2 ==0)
>>> a
<generator object <genexpr> at 0x0000000000D380F8>
>>> for i in a : print(i)
...
4
8
12
16
20
>>>

當generator的使用環境比較復雜,無法通過修改for循環的生成式來創建生成器滿足需求時,可以引入函數。

創建一個普通的計算二進制計算循環函數:

>>> def binary(maxnum):
... n=1
... count=0
... while count<=maxnum:
... print(n)
... n*=2
... count+=1
...
>>> binary(8)
1
2
4
8
16
32
64
128
256
>>>
>>>

不難發現,這個函數計算的結果是有規則性的,可以根據前一個結果推算出后一個結果,那么這個函數距離generator已經非常近了,僅需一個yield即可。將print(n)改成yield(n)即可實現將函數變為生成器:

>>> def binary(maxnum):
... n=1
... count=0
... while count<=maxnum:
... yield n
... n*=2
... count+=1
... return ‘func run end’
>>> res=binary(8)
>>>
>>> for i in res:
... print(i)
...
1
2
4
8
16
32
64
128
256

以上可以看出,函數變成了可以使用for循環的可迭代對象,但是執行結果沒有return返回值,為什么呢?因為加入了yield關鍵字,把function變為了generator。

個人理解,yield在生成器中的起到斷點的作用,每次調用next()時,遇到yield語句就中斷,下一次再調用next()時再從上次中斷的yield處開始下一輪循環。(類似游戲中的存檔~)

再來看一個yield最簡單的用法:

>>>
>>> def test():
... print('first')
... yield 'a'
... print('second')
... yield 'b'
... print('third')
... yield 'c'
...
>>> res=test()
>>> next(res)
first
'a'
>>> next(res)
second
'b'
>>> next(res)
third
'c'
>>> next(res)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>

可以看出,每執行一次next()函數調用,遇到yield后,函數的執行便卡住返回,下一次調用時,再從上一次yield處開始。直至生成器內的元素遍歷完成后第四次調用next()返回StopIteration的報錯。

再對比一下上方的的for循環的函數式生成器,可以發現,使用for循環時,生成式元素遍歷完畢后,不會報錯,而使用next()函數調用時,元素遍歷完畢后再次next()會返回報錯StopIteration,這即是生成式的一種返回值,類似函數里的return code。可以根據返回值來定義一些后續的操作,再次拿上方的二進制計算生成器來舉例:

>>> def binary(maxnum):
... n=1
... count=0
... while count<=maxnum:
... yield n
... n*=2
... count+=1
... return 'func run end '
...
>>> res=binary(8)
>>>
>>> while True:
... try:
... x=next(res)
... print('current value:',x)
... except StopIteration as signal:
... print('Finally:',singal.value)
... break
...
current value: 1
current value: 2
current value: 4
current value: 8
current value: 16
current value: 32
current value: 64
current value: 128
current value: 256
Finally: func run end
>>>

解析:try + except 配合使用,將接收到StopIteration作為執行完成的信號,singal.value的值為函數生成式return的值。

三、迭代器

在python中,可以被for循環遍歷的對象,被稱為可迭代對象,Iterable。其中包括常見的數據類型,包括list、tuple、dict、set、str等,另一種則是generator和帶yield的generator function。
可以使用isinstance()判斷一個對象是否是Iterable對象。

>>>
>>> from collections import Iterable
>>> isinstance([],Iterable)
True
>>> isinstance((),Iterable)
True
>>> isinstance({},Iterable)
True
>>> isinstance('abcd',Iterable)
True
>>>

但是,Iterable對象並不代表着它就是迭代器Iterator。
只有能調用next()函數不斷返回下一個值的對象才能被稱為迭代器,因此,生成器絕對是迭代器,但迭代器不一定是生成器,這個概念要理清。可以使用isinstance()判斷一個對象是否是Iterator。

>>> from collections import Iterator  //從collection庫導入Iterator模塊
>>> isinstance({},Iterator)
False
>>> isinstance([],Iterator)
False
>>> isinstance((),Iterator)
False
>>> isinstance('abcd',Iterator)
False
>>>
>>> def test():
... n=0
... while True:
... n+=1
... yield n
... return 'calculate done'
...
>>> isinstance(test(),Iterator)
True
>>>

為什么list、tuple、dict、str都是Iterable可迭代對象,卻不是Iterator迭代器呢?因為在python中,關於Iterator對象的定義是:可以被next()函數調用並不斷返回下一個數據,直到沒有數據時StopIteration的一個類似有序序列的數據流,無法提前得知這個數據流的長度,只能不斷通過next()函數去惰性計算下一個元素,而不是一次性全部計算出。

有沒有辦法實現將list、tuple、dict、str轉換成成Iterator呢?

>>> from collections import Iterator
>>> isinstance(iter({}),Iterator)
True
>>> isinstance(iter([]),Iterator)
True
>>> isinstance(iter(()),Iterator)
True
>>> isinstance(iter('abcd'),Iterator)
True
>>>

使用Iter()函數,可以將Iterable對象轉換成Iterator。

附記:
python中的for循環本質上就是通過不斷調用next()函數實現的,例如,以下for循環和下方調用迭代器方式的循環,兩者本質上是完全一致的:

list=[1,2,3]

for i in list:
print(i)
from collections import Iterator
list=iter([1,2,3])
while True:
try:
i=next(list)
print(i)
except StopIteration:
break

Over,過幾天再總結歸納一下python裝飾器~


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com