python 黑科技之迭代器、生成器、裝飾器

概述

可迭代對象、迭代器和生成器這三個概念很容易混淆,前兩者通常不會區分的很明顯,只是用法上有區別。生成器在某種概念下可以看做是特殊的迭代器,它比迭代實現上更加簡潔。三者關系如圖:
Iterator,Generator
Iterator,Generator

可迭代對象

先說下上面三者的基礎:可迭代對象(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 也是一個裝飾器,它將原函數的元信息拷貝到裝飾器環境中,從而不會被所替換的新函數覆蓋掉。

有了裝飾器,我們就可以剝離出大量與函數功能本身無關的代碼,增加了代碼的重用性。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容