概述
可迭代對象、迭代器和生成器這三個概念很容易混淆,前兩者通常不會區分的很明顯,只是用法上有區別。生成器在某種概念下可以看做是特殊的迭代器,它比迭代實現上更加簡潔。三者關系如圖:
可迭代對象
先說下上面三者的基礎:可迭代對象(Iterable Object),簡單的來理解就是可以使用 for
來循環遍歷的對象。比如常見的 list、set和dict??梢杂靡韵路椒▉頊y試對象是否是可迭代
>>> from collections import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整數是否可迭代
False
迭代器
其實你對所有的可迭代對象調用 dir()
方法時,會發現他們都實現了 __iter__
方法。這樣就可以通過 iter(object)
來返回一個迭代器。
>>> x = [1, 2, 3]
>>> y = iter(x)
>>> type(x)
<class 'list'>
>>> type(y)
<class 'list_iterator'>
可以看到調用 iter()
之后,變成了一個 list_iterator
的對象。會發現增加了 __next__
方法。所有實現了 __iter__
和 __next__
兩個方法的對象,都是迭代器。
迭代器是帶狀態的對象,它會記錄當前迭代所在的位置,以方便下次迭代的時候獲取正確的元素。__iter__
返回迭代器自身,__next__
返回容器中的下一個值,如果容器中沒有更多元素了,則拋出StopIteration異常。
>>> x = [1, 2, 3]
>>> y = iter(x)
>>> next(y)
1
>>> next(y)
2
>>> next(y)
3
>>> next(y)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
具體的實現我沒有深入研究。但是我大膽的猜測一下...聯系操作系統中 printf(fmt,...)
的實現方式,其中是定義一個 va_list
來用于保存需要打印的 ...
信息的。然后實現了va_start()
va_end()
va_arg()
三個方法來不停地迭代式的打印信息。感興趣可以自己了解。
那回到Iterator
,如何判斷對象是否是迭代器,和判斷是否是可迭代對象的方法差不多,只要把 Iterable
換成 Iterator
。
Python的for
循環本質上就是通過不斷調用next()
函數實現的,舉個栗子,下面的代碼
x = [1, 2, 3]
for elem in x:
...
實際上執行時是

也就是先將可迭代對象轉化為Iterator
,再去迭代。應該是處于對內存的節省考慮。因為迭代器只有在你調用 next()
才會實際計算下一個值。
itertools
庫提供了很多常見迭代器的使用
>>> from itertools import count # 計數器
>>> counter = count(start=13)
>>> next(counter)
13
>>> next(counter)
14
無限循環序列:
>>> from itertools import cycle
>>> colors = cycle(['red', 'white', 'blue'])
>>> next(colors)
'red'
>>> next(colors)
'white'
>>> next(colors)
'blue'
>>> next(colors)
'red'
生成器
生成器和裝飾器是python中最吸引人的兩個黑科技,生成器雖沒有裝飾器那么常用,但在某些針對的情境下十分有效。
我們創建列表的時候,受到內存限制,容量肯定是有限的,而且不可能全部給他一次枚舉出來。這里可以使用列表生成式,但是它有一個致命的缺點就是定義即生成,非常的浪費空間和效率。
所以,如果列表元素可以按照某種算法推算出來,那我們可以在循環的過程中不斷推算出后續的元素,這樣就不必創建完整的list,從而節省大量的空間。在Python中,這種一邊循環一邊計算的機制,稱為生成器:generator。
要創建一個 generator
,最簡單的方法是改造列表生成式
>>> [x*x for x in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> (x * x for x in range(10))
<generator object <genexpr> at 0x03804630>
還有一個方法是生成器函數,同樣是通過 def
定義,然后通過 yield
來支持迭代器協議,所以比迭代器寫起來更簡單。
>>>def spam():
yield"first"
yield"second"
yield"third"
>>> spam
<function spam at 0x011F32B0>
>>> gen
<generator object spam at 0x01220B20>
>>> gen.next()
'first'
>>> gen.next()
'second'
>>> gen.next()
'third'
當然一般都是通過for來使用的,這樣不用關心StopIteration
的異常
>>>for x in spam():
print x
first
second
third
進行函數調用的時候,返回一個生成器對象。在使用 next()
調用的時候,遇到 yield 就返回,記錄此時的函數調用位置,下次調用 next()
時,從斷點處開始。
的確有時候,迭代器和生成器很難區分,如文章開頭所說,generator 是比 Iterator 更加簡單的實現方式。官方文檔有這么一句
Python’s generators provide a convenient way to implement the iterator protocol.
你完全可以像使用 iterator
一樣使用 generator
,當然除了定義。定義一個iterator
,你需要分別實現 __iter__()
方法和 __next__()
方法,但 generator
只需要一個小小的yield
。
generator 還有 send()
和 close()
方法,都是只能在next()調用之后,生成器出去掛起狀態時才能使用的。
生成器在Python中是一個非常強大的編程結構,可以用更少地中間變量寫流式代碼,此外,相比其它容器對象它更能節省內存和CPU,當然它可以用更少的代碼來實現相似的功能?,F在就可以動手重構你的代碼了,但凡看到類似:
def something():
result = []
for ... in ...:
result.append(x)
return result
都可以用生成器函數來替換:
def iter_something():
for ... in ...:
yield x
提示:python
是支持協程的,也就是微線程,就是通過 generator
來實現的。配合 generator
我們可以自定義函數的調用層次關系從而自己來調度線程。
斐波那契數列
下面用 普通函數,迭代器和生成器來實現斐波那契數列,區分三種
輸出數列的前N個數
函數方法
def fab(max):
n,a,b = 0,0,1
L = []
while n < max:
L.append(b)
a,b = b,a+b
n += 1
return L
這個不多說
Iterator方法
為了節省內存,和處于未知輸出的考慮,使用迭代器來改善代碼。
class fab(object):
'''
Iterator to produce Fibonacci
'''
def __init__(self,max):
self.max = max
self.n = 0
self.a = 0
self.b = 1
def __iter__(self):
return self
def __next__(self):
if self.n < self.max:
r = self.b
self.a,self.b = self.b,self.a + self.b
self.n += 1
return r
raise StopIteration('Done')
迭代器什么都好,就是寫起來不簡潔。所以用 yield
來改寫第三版。
Generator
def fab(max):
n,a,b = 0,0,1
while n < max:
yield b
a,b = b,a+b
n += 1
使用下面來輸出
for a in fab(8):
print(a)
看起來很簡潔,而且有了迭代器的特性。
裝飾器
裝飾器(Decorator)是python中最吸引人的特性,裝飾器本質上還是一個函數,它可以讓已有的函數不做任何改動的情況下增加功能。
非常適合有切面需求的場景,比如權限校驗,日志記錄和性能測試等等。比如你想要執行某個函數前記錄日志或者記錄時間來統計性能,又不想改動這個函數,就可以通過裝飾器來實現。
不用裝飾器,我們會這樣來實現在函數執行前插入日志
def foo():
print('i am foo')
def foo():
print('foo is running')
print('i am foo')
雖然這樣寫是滿足了需求,但是改動了原有的代碼,如果有其他的函數也需要插入日志的話,就需要改寫所有的函數,不能復用代碼。可以這么寫
def use_logg(func):
logging.warn("%s is running" % func.__name__)
func()
def bar():
print('i am bar')
use_log(bar) #將函數作為參數傳入
這樣寫的確可以復用插入的日志,缺點就是顯示的封裝原來的函數,我們希望透明的做這件事。用裝飾器來寫
bar = use_log(bar)def use_log(func):
def wrapper(*args,**kwargs):
logging.warn('%s is running' % func.__name___)
return func(*args,**kwargs)
return wrapper
def bar():
print('I am bar')
bar = use_log(bar)
bar()
use_log()
就是裝飾器,它把真正我們想要執行的函數 bar()
封裝在里面,返回一個封裝了加入代碼的新函數,看起來就像是 bar()
被裝飾了一樣。這個例子中的切面就是函數進入的時候,在這個時候,我們插入了一句記錄日志的代碼。這樣寫還是不夠透明,通過@語法糖來起到 bar = use_log(bar)
的作用。
bar = use_log(bar)def use_log(func):
def wrapper(*args,**kwargs):
logging.warn('%s is running' % func.__name___)
return func(*args,**kwargs)
return wrapper
@use_log
def bar():
print('I am bar')
@use_log
def haha():
print('I am haha')
bar()
haha()
這樣看起來就很簡潔,而且代碼很容易復用。可以看成是一種智能的高級封裝。
裝飾器也是可以帶參數的,這位裝飾器提供了更大的靈活性。
def use_log(level):
def decorator(func):
def wrapper(*args, **kwargs):
if level == "warn":
logging.warn("%s is running" % func.__name__)
return func(*args)
return wrapper
return decorator
@use_log(level="warn")
def foo(name='foo'):
print("i am %s" % name)
foo()
實際上是對裝飾器的一個函數封裝,并返回一個裝飾器。這里涉及到作用域的概念,之前有一篇博客提到過??梢园阉闯梢粋€帶參數的閉包。當使用 @use_log(level='warn')
時,會將 level
的值傳給裝飾器的環境中。它的效果相當于 use_log(level='warn')(foo)
,也就是一個三層的調用。
這里有一個美中不足,decorator
不會改變裝飾的函數的功能,但會悄悄的改變一個 __name__
的屬性(還有其他一些元信息),因為 __name__
是跟著函數命名走的。可以用 @functools.wraps(func)
來讓裝飾器仍然使用 func
的名字。比如
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
functools.wraps 也是一個裝飾器,它將原函數的元信息拷貝到裝飾器環境中,從而不會被所替換的新函數覆蓋掉。
有了裝飾器,我們就可以剝離出大量與函數功能本身無關的代碼,增加了代碼的重用性。