主頁 > 後端開發 > 周末加班想摸魚?不如來點Python進階干貨呀!【超詳細迭代器、生成器、裝飾器使用教程】

周末加班想摸魚?不如來點Python進階干貨呀!【超詳細迭代器、生成器、裝飾器使用教程】

2021-04-26 19:08:26 後端開發

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 回圈的迭代就是通過使用迭代器來完成的,它在背后所做的事情是:

  1. 對一個容器呼叫 iter() 函式,獲取到該容器的迭代器
  2. 每次回圈時對迭代器呼叫 next() 函式,以獲取一個值
  3. 若捕獲到 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__() 方法,則可以說它支持迭代協議,

判斷一個已有的物件是否是可迭代的,有兩個方法:

  1. 通過內置函式 dir() 獲取這個物件所有方法,檢查是否有 '__iter__'

    >>> ‘__iter__’ in dir(list)
    True
    >>> ‘__iter__’ in dir(int)
    False

  2. 使用內置函式 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]

這里的閱讀順序是:

  1. for e in range(1, 11)
  2. if e % 2 == 1
  3. 2 ** 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

上一篇:jsonpath提取王者全英雄名字

下一篇:Java多對多網路通訊的實作

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more