python中“生成器”、“迭代器”、“閉包”、“裝飾器”的深入理解

一、生成器

1、什麼是生成器?

在python中,一邊循環一邊計算的機制,稱爲生成器:generator.

2、生成器有什麼優點?

  1、節約內存。python在使用生成器時對延遲操作提供了支持。所謂延遲,是指在需要的時候才產生結果,而不是立即產生結果。這樣在需要的時候纔去調用結果,而不是將結果提前存儲起來要節約內存。比如用列表的形式存放較大數據將會佔用不少內存。這是生成器的主要好處。比如大數據中,使用生成器來調取數據結果而不是列表來處理數據,因爲這樣可以節約內存。

  2、迭代到下一次的調用時,所使用的參數都是第一次所保留下的。

3、在python中創建生成器

在python中,有兩種創建生成器的方式:

  1、生成器表達式

類似與列表推導,但是,生成器返回按需產生結果的一個對象,而不是一次構建一個結果列表。

使用列表推導,將會一次返回所有結果:

1 li=[i**2 for i in range(5)]
2 print(li)
3 
4 [0, 1, 4, 9, 16]
5 [Finished in 0.3s]

將列表推導的中括號,替換成圓括號,就是一個生成器表達式:

1 li=(i**2 for i in range(5))
2 print(li)
3 for x in range(5):
4     print(next(li))

輸出結果爲:

複製代碼

1 <generator object <genexpr> at 0x0000000001E18938>
2 0
3 1
4 4
5 9
6 16
7 [Finished in 0.3s]

複製代碼

注意:創建完成生成器後,可以使用next()來調用生成器的數據結果,每調用一次返回一個值,直到調用結束。調用結束後li中爲空的,不在有任何值。要想使用它,只能重新創建新的生成器。(生成器表達式的第四行代碼也可以改成:print(x).)

  2、生成器函數

常規函數定義,但是使用yield語句而不是return語句返回結果。yield語句每次返回一個結果,但每個結果中間,掛起函數的狀態,以便下次從它離開的地方繼續執行。

我們下面來看一個例子。下面爲一個可以無窮生產奇數的生成器函數。

複製代碼

 1 def odd():
 2     n=1
 3     while True:
 4         yield n
 5         n+=2
 6 odd_num=odd()
 7 count=0
 8 for o in odd_num:
 9     if count >=5:
10         break
11     print(o)
12     count += 1

複製代碼

輸出結果爲:

1 1
2 3
3 5
4 7
5 9
6 [Finished in 0.3s]

上面的函數中,yield是必備的,當一個普通函數中包含yield時,系統會默認爲是一個generator。

再舉一個例子。使用生成器函數來生成斐波納契數列。

複製代碼

 1 def fib(times):
 2     n = 0
 3     a,b = 0,1
 4     while n < times:
 5         yield b
 6         a,b = b,a + b
 7         n += 1
 8     return "done"
 9 f = fib(5)
10 for x in f:
11     print(x)

複製代碼

輸出結果爲:

1 1
2 1
3 2
4 3
5 5
6 [Finished in 0.3s]

生成器的兩種方法:__next__() 和 send() 方法。

其中,__next__() 方法和next的作用是一樣的。如下所示。

複製代碼

 1 def fib(times):
 2     n=0
 3     a,b=0,1
 4     while n < times:
 5         yield b
 6         a,b=b,a+b
 7         n+=1
 8 f = fib(5)
 9 for i in range(5):
10     print(f.__next__())

複製代碼

輸出結果爲:

1 1
2 1
3 2
4 3
5 5
6 [Finished in 0.3s]

從上面的程序中可以看出,f._next_() 和 next(f) 是作用是一樣的。

下面再來看看send() 方法的使用。

複製代碼

 1 def fib(times):
 2     n = 0
 3     a,b = 0,1
 4     while n < times:
 5         temp = yield b 
 6         print(temp)
 7         a,b = b,a+b
 8         n += 1
 9 f = fib(5)
10 print(f.__next__())
11 print(f.send("haha"))
12 print(f.send("wangji"))

複製代碼

輸出結果爲:

1 1
2 haha
3 1
4 wangji
5 2
6 [Finished in 0.3s]

從上面代碼可以看出:使用send()時,必須在yield前面加上一個變量,並打印這個變量。在調用send()方法時其前面需要至少使用一次next()或__next__()方法,因爲生成器不可以在使用之前導入任何非None參數。由此可以知道,send()是用來向生成器中導入參數並返回該參數的,與該參數一起返回的還有生成器中原先保存的數據。

4、再看生成器

前面已經對生成器有了感性的認識,我們以生成器函數爲例,再來深入探討一下python的生成器:

  1、語法上和函數類似:生成器函數和常規函數幾乎是一樣的。它們都是使用def語句進行定義,差別在於,生成器使用yield語句返回一個值,而常規函數使用return語句返回一個值。

  2、自動實現迭代器協議:對於生成器,python會自動實現迭代器協議,以便應用到迭代北京中(如for循環,sum函數)。由於生成器自動實現了迭代器協議,所以,我們可以調用它的next方法,並且,在沒有值可以返回的時候,生成器自動產生StopIteration異常

  3、狀態掛起:生成器使用yield語句返回一個值。yield語句掛起該生成器函數的狀態,保留足夠的信息,以便以後從它離開的地方繼續執行

5、示例

我們再來看兩個生成器的例子,以便大家更好的理解生成器的作用。

首先,生成器的好處是延遲計算,一次返回一個結果。也就是說,它不會一次生成所有的結果,這對於大數據量處理,將會非常有用。

大家可以在自己的電腦上試試下面兩個表達式,並且觀察內存佔用情況。對於前一個表達式,我在自己的電腦上進行測試,還沒有看到最終結果電腦就已經卡死,對於後一個表達式,幾乎沒有什麼內存佔用。

1 a=sum([i for i in range(1000000000)])
2 print(a)
3 
4 b=sum(i for i in range(1000000000))
5 print(b)

除了延遲計算,生成器還能有效提高代碼可讀性。例如,現在有一個需求,求一段文字中,每個單詞出現的位置。

不使用生成器的情況:

複製代碼

1 def index_words(text):
2     result = []
3     if text:
4         result.append(0)
5     for index,letter in enumerate(text,1):
6         if letter == " ":
7             result.append(index)
8     return result

複製代碼

使用生成器的情況:

1 def index_word(text):
2     if text:
3         yield 0
4     for index,letter in enumerate(text,1):
5         if letter == " ":
6             yield index

這裏,至少有兩個充分的理由說明,使用生成器比不使用生成器代碼更加清晰:

  1、使用生成器以後,代碼行數更少。大家要記住,如果想把代碼寫的Pythonic,在保證代碼可讀性的前提下,代碼行數越少越好。

  2、不使用生成器的時候,對於每次結果,我們首先看到的是result.append(index),其次,纔是index。也就是說,我們每次看到的是一個列表的append操作,只是append的是我們想要的結果。使用生成器的時候,直接yield  index,少了列表append操作的干擾,我們一眼就能夠看出,代碼是要返回Index。

這個例子充分說明了,合理使用生成器,能夠有效提高代碼可讀性。只要大家完全接受了生成器的概念,理解了yield語句和return語句一樣,也是返回一個值。那麼,就能夠理解爲什麼使用生成器比不使用生成器要好,能夠理解使用生成器真的可以讓代碼變得清晰易懂。

6、使用生成器的注意事項

相信通過這篇文章,大家已經能夠理解生成器的作用和好處了。但是,還沒有結束,使用生成器,也有一點注意事項。

我們直接來看例子,假設文件中保存了省份的人口總數,現在,需要求每個省份的人口占全國總人口的比例。顯然,我們需要先求出全國的總人口,然後在遍歷每個省份的人口,用每個省的人口數除以總人口數,就得到了每個省份的人口占全國人口的比例。

如下所示:

複製代碼

 1 def get_province_population(filename):
 2     with open(filename) as f:
 3         for line in f:
 4             yield int(line)
 5 
 6 gen=get_province_population("data.txt")
 7 all_population = sum(gen)
 8 #print all_population
 9 for population in gen:
10     print(population/all population)

複製代碼

執行上面的這段代碼將不會有任何輸出,這是因爲,生成器只能遍歷一次。在我們執行sum語句的時候,就遍歷了我們的生成器,當我們再次遍歷我們的生成器的時候,將不會有任何記錄。所以,上面的代碼不會有任何輸出。

因此,生成器的唯一注意事項就是:生成器只能遍歷一次

7、總結

本文深入淺出地介紹了Python中,一個容易被大家忽略的重要特性,即Python的生成器。在實際工作中,充分利用Python生成器,不但能夠減少內存使用,還能夠提高代碼可讀性。掌握生成器也是Python高手的標配。希望本文能夠幫助大家理解Python的生成器。

 

二、迭代器

迭代是訪問集合元素的一種方式。迭代器是一個可以記住遍歷位置的對象。迭代器只能往前不能後退。

1、什麼是可迭代對象(Iterable)

  —  集合數據類型,如 list 、tuple、dict、set、str 等

  —  生成器和帶yield 的generator function

2、如何判斷對象可迭代?

  —  from collections import Iterable

  —  isinstance([ ],Iterable)

              圖2-1 命令行窗口下的操作圖

  如上,命令行模式下,先導入Iterable模塊,然後輸入isinstance([ ],Iterable),括號中前面爲待判斷的對象,結果以布爾類型結束(True或False),列表是可迭代對象,因此返回True。注意:整數是不可迭代對象。

3、什麼是迭代器呢?

迭代器是可以被next() 函數調用並不斷返回下一個值的對象稱爲迭代器。因此生成器是迭代器的子類,但是注意集合類型的可迭代對象不是迭代器。

  —  from collections import Iterator

  —  isinstance((x for x in range(10)),Iterator)

  這兩行代碼是用來判斷是否爲迭代器的,返回True或False。

iter()函數:將可迭代對象轉換成迭代器。

                圖2-2 迭代器的判斷

三、閉包

1、什麼是閉包?

內部函數對外部函數作用域裏變量的引用(非全局變量),則稱內部函數爲閉包。

閉包三要素:

      1、嵌套函數 

      2、變量的引用

      3、返回內部函數

舉例如下:

複製代碼

 1 #定義一個嵌套函數(要素1)
 2 def test(num):
 3     def test_in(num_in):
 4         #內部函數引用外部函數的變量(非全局變量)(要素2)
 5         print("sum = %s"%(num + num_in))
 6         #返回的結果可以被打印出來
 7         return num,num_in
 8     #返回內部的函數(要素3)
 9     return test_in
10 #這裏的rtn就是test_in
11 rtn = test(10)
12 print(rtn)
13 #內部函數test_in傳參
14 print(rtn(20))

複製代碼

輸出結果爲:

1 <function test.<locals>.test_in at 0x000000000220E378>
2 sum = 30
3 (10, 20)
4 [Finished in 0.3s]

定義閉包時,必須滿足上面三個要素。

2、閉包的應用

複製代碼

1 #求一元一次方程的值,輸入x,求y
2 def fun(a,b):     #其中a,b是固定的
3     def fun_in(x):
4         return a*x+b
5     return fun_in
6 f = fun(3,5)
7 print(f(1))    #每次輸入不同的x值即可求出對應的y值
8 print(f(3))

複製代碼

輸出結果爲:

1 8
2 14
3 [Finished in 0.3s]

上面的函數中,利用閉包來求一元一次方程的值,更方便,直接輸入x的值即可求出對應的y的值。因爲這利用了閉包可以記住外部函數的參數的特性。

四、裝飾器

1、什麼是裝飾器(decorator)?

裝飾器其實就是一個閉包,把一個函數當作參數然後返回一個替代版函數。

裝飾器有2個特性:
  1、可以把被裝飾的函數替換成其他函數

  2、可以在加載模塊時候立即執行

舉例如下:

  A、裝飾器對無參數函數進行裝飾:

複製代碼

 1 #裝飾器本質就是一個閉包,
 2 #將函數當成參數傳進去
 3 def deco(fun):
 4     def inner():
 5         print("你想進行的操作1")
 6         print("你想進行的操作2")
 7         fun()
 8     return inner
 9 #@deco是一個“語法糖”
10 @deco
11 def test():
12     print("test函數的操作")
13 #這個test已經被裝飾了,不是原來的test
14 test()
15 #14行的test()等同於如下操作:
16 #rtn = deco(test)
17 #rtn()

複製代碼

輸出結果爲:

1 你想進行的操作1
2 你想進行的操作2
3 test函數的操作
4 [Finished in 0.3s]

2、裝飾器的功能有:

        1、引入日誌

        2、函數執行時間統計

        3、執行函數前預備處理

        4、執行函數後清理功能

        5、權限z校驗等場景

        6、緩存

3、裝飾器的分類

裝飾器可分爲對有無參數函數進行裝飾的裝飾器對有無返回值函數進行裝飾的裝飾器,組合起來一共有4種。即:裝飾器對無參數無返回值的函數進行裝飾,裝飾器對無參數有返回值的函數進行裝飾,裝飾器對有參數無返回值的函數進行裝飾,裝飾器對有參數有返回值的函數進行裝飾。 

下面舉一個裝飾器實際應用的例子。

  A、裝飾器對無參數函數進行裝飾

複製代碼

 1 #定義函數:完成包裹數據
 2 def makeBold(fn):
 3     def wrapped():
 4         return "<b>" + fn() + "</b>"
 5     return wrapped
 6 
 7 #定義函數:完成包裹數據
 8 def makeItalic(fn):
 9     def wrapped():
10         return "<i>" + fn() + "</i>"
11     return wrapped
12 
13 @makeBold
14 def test1():
15     return "hello world - 1"
16 
17 @makeItalic
18 def test2():
19     return "hello world - 2"
20 
21 @makeBold
22 @makeItalic
23 def test3():
24     return "hello world - 3"
25 
26 print(test1())
27 print(test2())
28 print(test3())

複製代碼

輸出結果爲:

1 <b>hello world - 1</b>
2 <i>hello world - 2</i>
3 <b><i>hello world - 3</i></b>
4 [Finished in 0.3s]

上面這個例子是做網頁的時候對字體進行設置,對test1()進行加粗,對test2()斜體處理,對test3()先斜體在加粗。注意:對一個函數可以同時使用多個裝飾器,裝飾順序由內而外。

  B、裝飾器對有參數函數進行裝飾

複製代碼

 1 #定義一個裝飾器
 2 def deco(func):
 3     def wrapper(a,b):    #內部函數的參數必須和被裝飾的函數保持一致
 4         print("添加的功能")
 5         func(a,b)
 6 
 7     return wrapper
 8 
 9 @deco
10 #有參數的函數
11 def sum(a,b):
12     print(a+b)
13 
14 sum(10,20)

複製代碼

輸出結果爲:

1 添加的功能
2 30
3 [Finished in 0.3s]

當裝飾器裝飾有參數的函數時,裝飾器內部的函數也必須帶有和其相同的參數,因爲被裝飾的參數會被當成參數傳進裝飾器的內部函數中,如果兩者的參數不一致,會報錯。

C、裝飾器對不定長參數函數進行裝飾

複製代碼

 1 from time import ctime,sleep
 2 #定義一個裝飾器,裝飾不定長參數函數
 3 def deco(func):
 4     def wrapper(*args,**kwargs):
 5         print("%s called at the %s"%(func.__name__,ctime()))
 6         func(*args,**kwargs)
 7     return wrapper
 8 
 9 @deco
10 def test1(a,b,c):
11     print(a+b+c)
12 
13 @deco
14 def test2(a,b):
15     print(a+b)
16 
17 test1(10,20,30)
18 sleep(2)
19 test1(30,40,50)
20 
21 sleep(1)
22 test2(10,20)

複製代碼

輸出結果爲:

複製代碼

1 test1 called at the Fri Nov 10 19:08:03 2017
2 60
3 test1 called at the Fri Nov 10 19:08:05 2017
4 120
5 test2 called at the Fri Nov 10 19:08:06 2017
6 30
7 [Finished in 3.3s]

複製代碼

 D、裝飾器對有返回值的函數進行裝飾

複製代碼

 1 from time import ctime,sleep
 2 #定義一個裝飾器,裝飾不定長參數函數
 3 def deco(func):
 4     def wrapper(*args,**kwargs):
 5         print("%s called at the %s"%(func.__name__,ctime()))
 6         return func(*args,**kwargs)
 7     return wrapper
 8 #上面的裝飾器是一個萬能的裝飾器,因爲它可以裝飾任意一種函數
 9 #包括有無參數函數和有無返回值函數
10 
11 @deco
12 def test():
13     return "---ha--ha---"
14 
15 t = test()
16 print(t)

複製代碼

輸出結果爲:

1 test called at the Fri Nov 10 19:19:20 2017
2 ---ha--ha---
3 [Finished in 0.3s]

上面的裝飾器是一個萬能的裝飾器,因爲其可以用來裝飾任何函數,包括有無參數函數和有無返回值函數。

 

原文地址:https://www.cnblogs.com/tianyiliang/p/7811041.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章