Python基礎及進階內容持續更新中!歡迎小伙伴們一起關注學習!
目錄
寫在前面
一、深入理解迭代器和生成器
1、什么是迭代?(Iteration)
2、迭代器(Iterator)
(1)for 回圈的迭代程序
(2)可迭代(Iterable)物件
(3)自定義迭代器
(4)迭代器的好處
3、生成器(Generator)
(1)生成器運算式(Generator Expression)
二、生成器運算式和串列生成式
1、串列生成式
(1)串列生成式的寫法
2、字典生成式
3、集合生成式
4、生成器運算式
三、給凡人添加超能力:入手裝飾器
1、函式中定義函式
2、函式回傳函式
3、什么是裝飾器?
4、自定義裝飾器
(1)裝飾器原理
5、functools.wraps 裝飾器
6、帶引數的裝飾器
(1)帶引數的裝飾器原理
寫在前面
Hello,你好呀!我是灰小猿!一個超會寫bug的程式猿!
有多少人和我一樣仍然在周末痛苦的加班作業中?哈哈哈,快來慢慢的讀一下這篇文章,一起慢慢的摸魚吧!

最近和大家分享了好幾篇有關Python從基礎到進階的文章,幫助了很多不知道如何如學習Python的小伙伴,同時也為大家總結了有關Python中常見報錯及其解決:感興趣想學習的小伙伴可以去看一下,同時也歡迎小伙伴們關注我一起學習,之后為大家更新更多干貨資源!傳送門:
Python入門及進階:
【全網力薦】堪稱最易學的Python基礎入門教程
萬字長文爆肝Python基礎入門【第二彈、超詳細資料型別總結】
諾,你們要的Python進階來咯!【函式、類進階必備】
常見報錯及解決:
全網最值得收藏的Python常見報錯及其解決方案,再也不用擔心遇到BUG了!
今天就繼續來和大家分享一下在Python基礎進階中有關迭代器、生成器、裝飾器的詳細使用教程,【備好收藏,長文預警!】

一、深入理解迭代器和生成器
1、什么是迭代?(Iteration)
剛聽到這個名詞可能很多小伙伴會問,什么是迭代呢?在編程中,迭代指的是通過重復執行某個操作,不斷獲取被迭代物件中的資料,這樣的每一次操作就是就是一次迭代,
簡而言之,迭代是遍歷的一種形式,例如我們之前所學習的 for 回圈,它能不斷從地從串列、元組、字串、集合、字典等容器中取出新元素,每次一個元素直至所有元素被取完,這種 for 回圈操作就是迭代,
>>> for item in [1, 2, 3, 4, 5]:
… print(item)
…
1
2
3
4
5
2、迭代器(Iterator)
迭代器是具有迭代功能的物件,我們使用迭代器來進行迭代操作,
串列、元組、字串、集合、字典這些容器之所以能被迭代,是因為對它們呼叫內置函式 iter() 將回傳一個迭代器,這個迭代器可被用于迭代操作,
iter() 的使用方法:
迭代器 = iter(容器)
>>> numbers = [1, 2, 3, 4, 5]
>>> iterator = iter(numbers)
>>> iterator
<list_iterator object at 0x1074f34a8>
上面的「list_iterator」便是串列的迭代器,這個迭代器可用于迭代串列中的所有元素,
要使用迭代器,只需對迭代器呼叫內置函式 next(),便可逐一獲取其中所有的值,
next() 的使用方法:
值 = next(迭代器)
對于上面的串列迭代器,可以像這樣使用它:
>>> next(iterator)
1
>>> next(iterator)
2
>>> next(iterator)
3
>>> next(iterator)
4
>>> next(iterator)
5
>>> next(iterator)
Traceback (most recent call last):
File “”, line 1, in
StopIteration
可以看到,每次呼叫 next() 將依次回傳串列中的一個值,直至所有的值被遍歷一遍,此時將拋出 StopIteration 例外以表示迭代終止,
(1)for 回圈的迭代程序
for 回圈的迭代就是通過使用迭代器來完成的,它在背后所做的事情是:
- 對一個容器呼叫
iter()函式,獲取到該容器的迭代器 - 每次回圈時對迭代器呼叫
next()函式,以獲取一個值 - 若捕獲到
StopIteration例外則結束回圈
(2)可迭代(Iterable)物件
并不是所有的物件都可以被 iter() 函式使用,比如整數:
>>> iter(123)
Traceback (most recent call last):
File “”, line 1, in
TypeError: ‘int’ object is not iterable
這里拋出 TypeError 例外,提示 int 物件不是可迭代的,
什么是可迭代(的)?
- 從表面來看,所有可用于
for回圈的物件是可迭代的,如串列、元組、字串、集合、字典等容器 - 從深層來看,定義了
__iter__()方法的類物件就是可迭代的,當這個類物件被iter()函式使用時,將回傳一個迭代器物件,如果物件具有__iter__()方法,則可以說它支持迭代協議,
判斷一個已有的物件是否是可迭代的,有兩個方法:
-
通過內置函式
dir()獲取這個物件所有方法,檢查是否有'__iter__'>>> ‘__iter__’ in dir(list)
True
>>> ‘__iter__’ in dir(int)
False -
使用內置函式
isinstance()判斷其是否為Iterable的物件from collections.abc import Iterable isinstance(物件, Iterable)>>> from collections.abc import Iterable
>>> isinstance([1, 2, 3], Iterable)
True
(3)自定義迭代器
我們可以自己來定義迭代器類,只要在類中定義 __next__() 和 __iter__() 方法即可,如:
class MyIterator:
def __next__(self):
代碼塊
def __iter__(self):
return self
我們來寫一個迭代器,這個迭代器從 2^0 開始回傳 2 的指數冪,至 2^10 終止,
class PowerOfTwo:
def __init__(self):
self.exponent = 0 # 將每次的指數記錄下來
def __next__(self):
if self.exponent > 10:
raise StopIteration
else:
result = 2 ** self.exponent # 以 2 為底數求指數冪
self.exponent += 1
return result
def __iter__(self):
return self
每次對迭代器使用內置函式 next() 時, next() 將在背后呼叫迭代器的 __next__() 方法,所以迭代器的重點便是 __next__() 方法的實作,在這個 __next__() 方法中,我們將求值時的指數記錄在物件屬性 self.exponent 中,求值結束時指數加 1,為下次求值做準備,
對于方法 __iter__() 的實作,我們直接回傳迭代器物件自身即可,有了這個方法,迭代器物件便是可迭代的,可直接用于 for 回圈,
擴展:如果物件具有
__iter__()和__next__()方法,則可以說它支持迭代器協議,
迭代器 PowerOfTwo 使用示例:
>>> p = PowerOfTwo()
>>> next(p)
1
>>> next(p)
2
>>> next(p)
4
>>> next(p)
8
>>> next(p)
16
>>> next(p)
32
>>> next(p)
64
>>> next(p)
128
>>> next(p)
256
>>> next(p)
512
>>> next(p)
1024
>>> next(p)
Traceback (most recent call last):
File “”, line 1, in
File “”, line 6, in next
StopIteration
這個迭代器當然也可用于 for 回圈:
>>> p = PowerOfTwo()
>>> for item in p:
… print(item)
…
1
2
4
8
16
32
64
128
256
512
1024
(4)迭代器的好處
- 一方面,迭代器可以提供迭代功能,當我們需要逐一獲取資料集合中的資料時,使用迭代器可以達成這個目的
- 另一方面,資料的存盤是需要占用記憶體的,資料量越大所占用的記憶體就越多,如果我們使用串列這樣的結構來保存大批量的資料,并且資料使用頻率不高的話,就十分浪費資源了,而迭代器可以不保存資料,它的資料可以在需要時被計算出來(這一特性也叫做惰性計算),在合適的些場景下使用迭代器可以節省記憶體資源,
3、生成器(Generator)
剛才我們自定義了迭代器,其實創建迭代器還有另一種方式,就是使用生成器,
生成器是一個函式,這個函式的特殊之處在于它的 return 陳述句被 yield 陳述句替代,
如剛才用于生成 2 的指數冪的迭代器,可以通過生成器來實作:
def power_of_two():
for exponent in range(11): # range(11) 表示左閉右開區間 [0, 11),不包含 11
yield 2 ** exponent # 以 2 為底數求指數冪
生成器使用方法:
p = power_of_two() # 以函式呼叫的方式創建生成器物件
next(p) # 同樣使用 next() 來取值
生成器的關鍵在于 yield 陳述句,yield 陳述句的作用和 return 陳述句有幾分相似,都可以將結果回傳,不同在于,生成器函式執行至 yield 陳述句,回傳結果的同時記錄下函式內的狀態,下次執行這個生成器函式,將從上次退出的位置(yield 的下一句代碼)繼續執行,當生成器函式中的所有代碼被執行完畢時,自動拋出 StopIteration 例外,
我們可以看到,生成器的用法和迭代器相似,都使用 next() 來進行迭代,這是因為生成器其實就是創建迭代器的便捷方法,生產器會在背后自動定義 __iter__() 和 __next__() 方法,
(1)生成器運算式(Generator Expression)
可以用一種非常簡便的方式來創建生成器,就是通過生成器運算式,生成器的寫法非常簡單,但是靈活性也有限,所能表達的內容相對簡單,
生成器運算式的寫法如下:
生成器 = (針對項的操作 for 項 in 可迭代物件)
如:
>>> letters = (item for item in ‘abc’)
>>> letters
<generator object at 0x1074a8228>
>>> next(letters)
‘a’
>>> next(letters)
‘b’
>>> next(letters)
‘c’
>>> letters = (i.upper() * 2 for i in ‘abc’)
>>> next(letters)
‘AA’
>>> next(letters)
‘BB’
>>> next(letters)
‘CC’
二、生成器運算式和串列生成式
1、串列生成式
如果我們想要構造一個包含指定元素或者具有某種規則的串列,比如 2 的指數冪序列 [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024], 該怎么做?
-
最簡單的辦法,直接將這些數原樣寫入代碼來創建串列:
nums = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]這種辦法當然是可行的,不過也有很大的局限,像這樣直接將資料寫入代碼的做法,叫做硬編碼,很多情況下我們不會使用硬編碼的方式來創建串列(或者其它容器),因為串列中有什么資料往往在寫代碼時是不能確定的,通常在程式運行程序中通過計算得到,或從程式外部讀入(比如從資料庫 / 檔案 / 網路中讀入),另外當資料量很大時,使用硬編碼也是一件繁瑣低效的事,
-
還有一種辦法,創建一個空的串列,之后通過計算(或其它操作)獲得各個元素,并添加到串列中:
nums = [] exponent = 1 while exponent <= 10: nums.append(2 ** exponent) exponent += 1>>> nums
[2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]這樣方式完全可行,也沒有什么缺陷,
-
或者,對一個現有的可迭代物件中的各個元素做處理,構造出一個新的串列:
nums = [] for i in range(1, 11): # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] nums.append(2 ** i)>>> nums
[2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]這段代碼可以用一種簡潔的方式寫出,只需要一行代碼:
nums = [2 ** i for i in range(1, 11)]>>> nums
[2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]這行代碼就是我們這個章節要所講的串列生成式,顧名思義,串列生成式最終生成的是一個串列,它是用已有的可迭代物件來構造新串列的便捷方法,
提示:如果不清楚什么是可迭代物件,可以看一下上一篇文章《深入理解迭代器和生成器》,
(1)串列生成式的寫法
串列生成式的語法如下:
[對項的操作 for 項 in 可迭代物件]
這個寫法怎么理解呢?
首先,這句代碼的閱讀順序是:for 項 in 可迭代物件 -> 對項的操作,其次,外圍的方括號([])表明這是串列生成式,最終的結果是一個串列,
for 項 in 可迭代物件 這部分和 for 回圈很相似,通過迭代可迭代物件,每次取出一個項,對于取出的項,我們可以對它做一些處理,也就是運算式中的 對項的操作 部分,最終,可迭代物件中的所有項都會被迭代和處理,并被收集起來形成一個新的串列,
這個程序用偽代碼來描述的話是這樣的:
串列 = []
for 項 in 可迭代物件:
新項 = 對項的操作(項)
串列.appent(新項)
來看一個例子:
這里有個串列:['a', 'b', 'c', 'd', 'e'],怎樣把其中的每個小寫字母轉換為大寫?可以這樣:
[char.upper() for char in ['a', 'b', 'c', 'd', 'e']]
>>> [char.upper() for char in [‘a’, ‘b’, ‘c’, ‘d’, ‘e’]]
[‘A’, ‘B’, ‘C’, ‘D’, ‘E’]
如果你不能一下子理解,不妨比較一下用 for 回圈來實作的版本,它們之間是等價的:
chars = []
for char in ['a', 'b', 'c', 'd', 'e']:
chars.append(char.upper())
>>> chars
[‘A’, ‘B’, ‘C’, ‘D’, ‘E’]
再來談談 [對項的操作 for 項 in 可迭代物件] 中的 對項的操作,這個操作它可以簡單,也可以很復雜,
簡單來看,我們可以直接使用 項 本身而不做任何處理,如:
>>> [char for char in ‘ABCDEF’]
[‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ‘F’]
當然如果是要得到這個結果,我們應該直接使用 list('ABCDEF'):
>>> list(‘ABCDEF’)
[‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ‘F’]
復雜來看,我們可以對 項 進行一系列的處理,如分別將 'abcde' 中每個字母的大寫形式和小寫形式放到元組中:
[(char.upper(), char) for char in 'abcde']
>>> [(char.upper(), char) for char in ‘abcde’]
[(‘A’, ‘a’), (‘B’, ‘b’), (‘C’, ‘c’), (‘D’, ‘d’), (‘E’, ‘e’)]
這里我們將每個 char 轉換為了 (char.upper(), char),并且其中 char 被多次用到,
上個例子等價于:
result = []
for char in 'abcde':
result.append((char.upper(), char))
>>> result
[(‘A’, ‘a’), (‘B’, ‘b’), (‘C’, ‘c’), (‘D’, ‘d’), (‘E’, ‘e’)]
串列生成式中使用 if
在串列生成式的中,每次迭代的 項 是可以被篩選過濾的,使用 if 關鍵字,如:
[對項的操作 for 項 in 可迭代物件 if 對項的判斷]
它的閱讀順序是:for 項 in 可迭代物件 -> if 對項的判斷 -> 對項的操作,
每次迭代時所取出的 項,要先經過 對項的判斷,如果結果為 True,才會由 對項的操作 處理,如果 對項的判斷 的結果為 False,后續 對項的操作 會被跳過,此時最終串列的長度也會減少,
舉個例子,[2 ** i for i in range(1, 11)] 可以生成出 20~210 間的整數,如果我們只想要其中的奇數次方的值,該怎么做?
這時就可以在串列中使用 if 關鍵字:
[2 ** i for i in range(1, 11) if i % 2 == 1 ]
>>> [2 ** i for i in range(1, 11) if i % 2 == 1 ]
[2, 8, 32, 128, 512]
這里的閱讀順序是:
for e in range(1, 11)if e % 2 == 12 ** e
上述代碼等價于:
nums = []
for i in range(1, 11):
if i % 2 == 1:
nums.append(2 ** i)
>>> nums
[2, 8, 32, 128, 512]
串列生成式中嵌套 for
串列生成式中的 for 中還可以再嵌套 for,如:
[對項1和(或)項2的操作 for 項1 in 可迭代物件1 for 項2 in 可迭代物件2]
它等價于:
串列 = []
for 項1 in 可迭代物件1:
for 項2 in 可迭代物件2:
新項 = 對項1和(或)項2的操作()
串列.append(新項)
看起來有點復雜,我們看個例子:
nums = [1, 2, 3]
chars = ['a', 'b', 'c']
[c * n for n in nums for c in chars]
>>> nums = [1, 2, 3]
>>> chars = [‘a’, ‘b’, ‘c’]
>>> [c * n for n in nums for c in chars]
[‘a’, ‘b’, ‘c’, ‘aa’, ‘bb’, ‘cc’, ‘aaa’, ‘bbb’, ‘ccc’]
它等價于:
nums = [1, 2, 3]
chars = ['a', 'b', 'c']
result = []
for n in nums:
for c in chars:
result.append(c * n)
>>> result
[‘a’, ‘b’, ‘c’, ‘aa’, ‘bb’, ‘cc’, ‘aaa’, ‘bbb’, ‘ccc’]
[對項1和(或)項2的操作 for 項1 in 可迭代物件1 for 項2 in 可迭代物件2] 中的 可迭代物件2 可以是 項1 本身,也就是可以寫成:
[對項和(或)子項的操作 for 項 in 可迭代物件 for 子項 in 項]
例如:
strings = ['aa', 'bb', 'cc']
[char for string in strings for char in string]
>>> strings = [‘aa’, ‘bb’, ‘cc’]
>>> [char for string in strings for char in string]
[‘a’, ‘a’, ‘b’, ‘b’, ‘c’, ‘c’]
它等價于:
strings = ['aa', 'bb', 'cc']
result = []
for string in strings:
for char in string:
result.append(char)
>>> result
[‘a’, ‘a’, ‘b’, ‘b’, ‘c’, ‘c’]
2、字典生成式
便捷地構造串列可以使用串列生成式,同樣的,想要通過已有的可迭代物件來便捷地構造字典,可以使用字典生成式,
字典生成式的寫法是:
{鍵: 值 for 項 in 可迭代物件}
和串列生成式非常相似,不同之處在于它使用的是花括號({}),另外還使用 鍵: 值 形式,
舉個例子,有字串 'abcde',以每個小字母作為鍵,對應大寫字母作為值的來構造個字典:
{char: char.upper() for char in 'abcde'}
>>> {char: char.upper() for char in ‘abcde’}
{‘a’: ‘A’, ‘b’: ‘B’, ‘c’: ‘C’, ‘d’: ‘D’, ‘e’: ‘E’}
同樣的,字典生成式中也可以使用 if 和嵌套 for,使用方法參照串列生成式,
3、集合生成式
想要通過已有的可迭代物件來構造集合,可以使用集合生成式,
你可能已經猜到了,只需要將串列生成式的方括號([])替換為花括號({})即可:
{對項的操作 for 項 in 可迭代物件}
例如:
{char.lower() for char in 'ABCDABCD'}
>>> {char.lower() for char in ‘ABCDABCD’}
{‘c’, ‘a’, ‘d’, ‘b’}
提示:通過這個例子也能看到集合的重要特性——無序且無重復,
同樣的,集合生成式中也可以使用 if 和嵌套 for,
4、生成器運算式
上面有串列生成式、字典生成式、集合生成式,那么是不是也有「元組生成式」?是不是用圓括號來表示就可以了?
不是的,Python 中并沒有「元組生成式」!雖然 Python 中確實有類似的圓括號的寫法:
(對項的操作 for 項 in 可迭代物件)
但這可不是什么「元組生成式」,而是我們上一章節學習過的生成器運算式,
生成器運算式是一種創建生成器的便捷方法,雖然寫法上和串列生成式、字典生成式、集合生成式相似,卻有著本質的不同,因為它創建出來的是生成器,而不是串列、字典、集合這類容器,
(char.lower() for char in 'ABCDEF')
>>> g = (char.lower() for char in ‘ABCDEF’)
>>> g
<generator object at 0x103da6c78>
>>> next(g)
‘a’
>>> next(g)
‘b’
提示:如果你對生成器有些遺忘,不妨看下前一篇文章《深入理解迭代器和生成器》,
生成器運算式中同樣可以使用 if 和嵌套 for,使用方法和串列生成式相同,
三、給凡人添加超能力:入手裝飾器
在學習裝飾器前,我們先來了解兩個函式概念,

1、函式中定義函式
在 Python 中,函式內部是可以嵌套地定義函式的,如:
def print_twice(word):
def repeat(times):
return word * times
print(repeat(2))
>>> print_twice('go ')
gogo
內層函式只能在包裹它的外層函式中使用,而不能在外層函式外使用,比如上面的 repeat() 可以在 print_twice() 中使用,但是不能在 print_twice() 的外部使用,
另外,內層函式中可以使用外層函式的引數或其它變數,如上面的引數 word,
2、函式回傳函式
之前我們學習過,函式可以作為另一個函式的引數,類似的,函式的回傳值也可以是一個函式,
如:
def print_words(word):
def repeat(times):
return word * times
return repeat
>>> f = print_words(‘go’)
>>> f
<function print_words..repeat at 0x10befe620>
我們呼叫 print_words() 并用變數 f 接收其回傳值,f 是個函式,是 print_words 下的 repeat 函式,
既然 f 是個函式,自然可以被呼叫,這也就相當于呼叫 repeat():
>>> f(2)
‘gogo’
擴展:我們直接呼叫
f(也就是repeat())時,repeat()內部會使用變數word,而這個變數時定義在外層函式print_words()中的,卻會一直伴隨repeat()而存在,這在 Python 中叫作閉包,
3、什么是裝飾器?
好了,回到正題,來看看什么是裝飾器,我們在《類進階》章節中介紹過類方法和靜態方法的定義方式,還記得嗎,定義它們時需要用到 @classmethod 和 @staticmethod,它們就是裝飾器,寫法為 @裝飾器名稱,
裝飾器用來增強一個現有函式的功能,并且不改變這個函式的呼叫方式,這種增強是非侵入式的,也就是說無需直接修改函式內部的代碼,而是在函式的外部做文章,
舉個例子,假設我們有這樣一個函式:
def say_hello():
print('Hello!')
>>> say_hello()
Hello!
這個函式非常簡單,每次呼叫會輸出「Hello!」,假如我們想在每次輸出「Hello!」的同時附帶上當前的時間,像這樣:
>>> say_hello()
[ 2019-09-14 16:38:10.942802 ]
Hello!
>>> say_hello()
[ 2019-09-14 16:42:58.409742 ]
Hello!
如果想具備上面的功能,但又不想修改 say_hello() 函式的內部實作,該怎么做?
這就是裝飾器的典型使用場景了——非侵入的情況下讓函式具備更多的功能,
假設我們已經有了一個能滿足該需求的裝飾器 @time ,只要像這樣來裝飾 say_hello() 即可:
@time
def say_hello():
print('Hello!')
函式的呼叫方式依然不變:
>>> say_hello()
當然,雖然 Python 中內置有一些裝飾器,如 @classmethod、@staticmethod,但并沒 @time,所以我們需要自己來定義它,
4、自定義裝飾器
我們來自定義之前所說的裝飾器 @time,要求是使用它可以在函式呼叫時輸出呼叫時間,
這里直接給出 @time 的實作:
import datetime # 日期時間相關庫,用于后續獲取當前時間
def time(func):
def wrapper(*args, **kw):
print('[', datetime.datetime.now(), ']')
return func(*args, **kw)
return wrapper
我們暫且不關注具體的實作細節,先使用一下看看:
@time
def say_hello():
print('Hello!')
>>> say_hello()
[ 2019-09-14 16:42:58.409742 ]
Hello!
>>> say_hello()
[ 2019-09-15 09:44:06.155869 ]
Hello!
沒有問題,效果和預期相同!那這是什么原理呢?
(1)裝飾器原理
其實,
@time
def say_hello():
print('Hello!')
等效于:
def say_hello():
print('Hello!')
say_hello = time(say_hello)
也就是說,我們用 @time 裝飾 say_hello() 時,Python 會在背后做了這樣一個操作(重點):
say_hello = time(say_hello)
@time(包括所有裝飾器)本質上是個以函式作為引數,并回傳函式的函式,不妨回過頭來觀察下 @time 實作:
import datetime # 日期時間相關庫,用于后續獲取當前時間 def time(func): def wrapper(*args, **kw): print('[', datetime.datetime.now(), ']') return func(*args, **kw) return wrapper
say_hello = time(say_hello) 這句代碼將函式 say_hello 作為引數來呼叫 time(),time() 將其內部定義的函式回傳了出來,并替換了函式 say_hello,結合裝飾器實作來看, say_hello() 其實變成了 time() 中的 wrapper(),
>>> say_hello
<function time..wrapper at 0x10befea60>
那就來具體看下 wrapper():
def wrapper(*args, **kw):
print('[', datetime.datetime.now(), ']')
return func(*args, **kw)
wrapper() 其實也非常簡單,其內部的 print('[', datetime.datetime.now(), ']') 以 [ 時間 ] 的格式將當前時間輸出出來,達成了「輸出函式呼叫時間」的目的,其中 datetime.datetime.now() 用于獲取當前的時間,
最后一句 return func(*args, **kw) 比較關鍵,這里呼叫函式 func() 并將其結果回傳出去,func() 是什么?它就是 say_hello(),最初 say_hello 作為引數被傳入 time() 中,其引數名便是 func,
引數 *args 和 **kw 是什么?還記得我們在《函式進階》中的內容嗎,*args 可以接收一切非關鍵字引數,而 **kw 可以接收一切關鍵字引數,兩個結合起來一起使用就可以接收一切引數了,用在這里的作用是,接收呼叫 say_hello() 時的所有引數,并悉數傳給 func(),
稍作梳理我們就能明白,裝飾器之所以能夠增強一個函式的功能,其實就是將被裝飾函式用新函式替換,雖然還是同一個函式名,但函式內部實作已經變了,而這個新函式的內部在添加了一些功能的后,還會呼叫之前被裝飾的函式,這樣就相當于對被裝飾的函式做了非侵入的擴展,
5、functools.wraps 裝飾器
當一個函式不被裝飾器裝飾時,其函式名稱就是自己,如:
>>> def say_hello():
… print(‘Hello!’)
…
>>> say_hello
<function say_hello at 0x10efbb1e0>>>> say_hello.__name__
‘say_hello’
在解釋器中直接輸入 say_hello,顯示其為 function say_hello,使用 say_hello.__name__,可以直接獲取到其函式名稱,此處顯示為 say_hello,
如果我們用裝飾器 @time 來修飾這個函式,那結果就不同了:
>>> @time
… def say_hello():
… print(‘Hello!’)
…
>>> say_hello
<function time..wrapper at 0x10efbb048>>>> say_hello.__name__
‘wrapper’
可以看到其名字資訊被裝飾器中的函式 wrapper 覆寫了,
是的,由于裝飾器本質上是用一個新的函式來替換被裝飾的函式,所以函式的元資訊會被覆寫,
那有沒有什么方式保留被裝飾函式的元資訊呢?有的,可以在定義裝飾器時使用 @functools.wraps 裝飾器,使用如下:
import datetime
import functools
def time(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('[', datetime.datetime.now(), ']')
return func(*args, **kw)
return wrapper
>>> say_hello
<function say_hello at 0x10ef5c378>>>> say_hello.__name__
‘say_hello’
可以看到使用 @functools.wraps 后,元資訊恢復如初,不留痕跡,
6、帶引數的裝飾器
既然裝飾器本質上是個函式,那這個函式能不能有引數呢?答案是可以有,
舉個例子,剛才我們輸出的時間格式是 [ 2019-09-14 16:42:58.409742 ],如果我們想要自行指定這個格式,可以考慮用裝飾器引數的形式來設定,
帶時間格式的裝飾器如下:
import datetime
import functools
def time(format):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print(datetime.datetime.now().strftime(format))
return func(*args, **kw)
return wrapper
return decorator
可以看到,這回裝飾器變成了三層函式嵌套的形式,是的,如果需要指定裝飾器的引數,那么就需要在原來裝飾器的基礎上在再加一層函式,
wrapper() 中原本的 print('[', datetime.datetime.now(), ']') 被修改為 print(datetime.datetime.now().strftime(format)),其中的 format 便是裝飾器的引數,也就是時間格式,
使用時,在裝飾器 @time 后添加括號并寫上引數:
@time('%Y/%m/%d %H:%M:%S')
def say_hello():
print('Hello!')
>>> say_hello()
2019/09/15 10:00:24
Hello!
可以看到時間格式已經根據我們的設定而生效,
擴展:
'%Y/%m/%d %H:%M'是datetime包中用于指定時間格式的字串,其中:
%Y表示年%m表示月%d表示天%H表示小時%M表示分鐘%S表示秒,
(1)帶引數的裝飾器原理
帶引數的裝飾器的實作為什么要三層函式嵌套?看了下面的等效代碼你就明白了!
@time('%Y/%m/%d %H:%M:%S')
def say_hello():
print('Hello!')
等效于:
def say_hello():
print('Hello!')
say_hello = time('%Y/%m/%d %H:%M:%S')(say_hello)
而不帶引數的裝飾器的等效代碼是 say_hello = time(say_hello),對比可以看出,帶引數的裝飾器的等效代碼多了一次函式呼叫,通過這種方式將裝飾器引數傳遞到內部的兩層函式中,這之后便回到了不帶引數的裝飾器的情形,
關于Python進階中迭代器、生成器、裝飾器的講解就和大家分享到這里,其中有不懂的地方歡迎小伙伴評論提出!
之后持續為大家分享更多進階內容,歡迎小伙伴們點贊關注呀!
灰小猿陪你一起進步!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/280368.html
標籤:python
下一篇:Java多對多網路通訊的實作
