第七章 函數(shù)式編程

7.1 又見函數(shù)

1.Python中的函數(shù)式

前面介紹了面向進(jìn)程和面向?qū)ο蟮木幊蹋@一章講解面向函數(shù)的編程即函數(shù)式編程。函數(shù)式編程的本質(zhì)在于封裝,它以函數(shù)為中心進(jìn)行代碼封裝,函數(shù)在面向進(jìn)程中涉及過,它有參數(shù)和返回值,起到輸入和輸出數(shù)據(jù)的功能。
函數(shù)式編程強(qiáng)調(diào)了函數(shù)的純粹性,一個(gè)純函數(shù)是沒有副作用的,即一個(gè)函數(shù)不會(huì)影響其他函數(shù)。我們?cè)谥暗目勺儗?duì)象中演示過,在函數(shù)內(nèi)部改變列表里面的對(duì)象,會(huì)影響函數(shù)外部的列表元素,其他調(diào)用該列表的函數(shù)也會(huì)產(chǎn)生相應(yīng)的影響,這就產(chǎn)生了副作用。所以,為了是函數(shù)純粹,我們?cè)诤瘮?shù)內(nèi)部使用的都是不可變更的變量。

由于Python中存在可變對(duì)象,因此只能盡量避免副作用。
函數(shù)式編程由于函數(shù)相互獨(dú)立,因此不用擔(dān)心函數(shù)調(diào)用對(duì)其他函數(shù)的影響,使用起來更加簡(jiǎn)單。
而且,純函數(shù)也方便進(jìn)行并行化運(yùn)算。

在進(jìn)行并行化編程時(shí),我們經(jīng)常擔(dān)心不同進(jìn)程之間相互干擾的問題。當(dāng)多個(gè)進(jìn)程或者線程同時(shí)修改一個(gè)變量時(shí),進(jìn)程或線程的先后順序會(huì)影響最終結(jié)果,比如

from threading import Thread 

x = 5

def double():
    global x  # 聲明全局變量x
    x = x * 2
    
def plus_ten():
    global x  # 聲明全局變量x
    x = x + 10
    
th1 = Thread(target = double)  # 線程1
th2 = Thread(target = plus_ten) # 線程2

th1.start()  # 線程1開始執(zhí)行
th2.start()  # 線程2開始執(zhí)行

th1.join()   # 線程1結(jié)束
th2.join()   # 線程2結(jié)束 

print(x)

結(jié)果:
20

將上面的線程調(diào)換一下
th2.start()  # 線程2開始執(zhí)行
th1.start()  # 線程1開始執(zhí)行

th2.join()   # 線程2結(jié)束 
th1.join()   # 線程1結(jié)束

print(x)

結(jié)果:
30

global是聲明全局變量的關(guān)鍵字,函數(shù)對(duì)全局變量的修改能被其他函數(shù)看見,因此造成了副作用。如果并行地執(zhí)行上面兩個(gè)函數(shù),執(zhí)行順序是不確定的,結(jié)果也不一樣。比如先執(zhí)行double,再執(zhí)行plus_ten,得到的結(jié)果是20;若先執(zhí)行plus_ten,再執(zhí)行double,得到的結(jié)果是30。這被稱為竟跑條件,是并行編程中需要盡力避免的。

而函數(shù)式編程消滅了副作用,也在無形之中消除了竟跑條件的可能。因此,函數(shù)式編程天然地適用于并行化運(yùn)算。
Python中加入了lambda函數(shù)、map、filter、reduce等高階函數(shù),引入了函數(shù)式編程的特征。

函數(shù)式編程的思路是自上而下的,先提出一個(gè)大問題,在最高層定義一個(gè)函數(shù)來解決這個(gè)大問題,在函數(shù)的內(nèi)部,再用其他函數(shù)來解決小問題。比如,我們要解決"如何把大象放進(jìn)冰箱"的問題,首先我們定義一個(gè)函數(shù)"把大象放進(jìn)冰箱"來解決大問題,然后在"把大象放進(jìn)冰箱"里面,有三步函數(shù)"打開冰箱門"、"放入大象"、"關(guān)上冰箱",如果需要繼續(xù)細(xì)化,則在這三步函數(shù)內(nèi)部繼續(xù)調(diào)用其他函數(shù)。

2.并行運(yùn)算

并行運(yùn)算是指多條指令同時(shí)執(zhí)行。對(duì)應(yīng)的串行運(yùn)算指的是同一時(shí)間只能執(zhí)行一條指令。

大規(guī)模的并行運(yùn)算一般是在多個(gè)主機(jī)組成的集群上進(jìn)行的,主機(jī)之間通過高速的網(wǎng)絡(luò)設(shè)備通信。但由于集群的成本過高,我們可以在單機(jī)上通過多線程或多進(jìn)程來模擬集群的并行處理。

在一臺(tái)單機(jī)上,往往運(yùn)行著多個(gè)程序,即進(jìn)程。如用瀏覽器上網(wǎng)時(shí),用網(wǎng)易云聽音樂,計(jì)算機(jī)就同時(shí)運(yùn)行兩個(gè)進(jìn)程。這就是單機(jī)"分時(shí)復(fù)用"的方式,把運(yùn)算能力分給多個(gè)進(jìn)程。
也就是說,集群和單機(jī)都實(shí)現(xiàn)了多個(gè)進(jìn)程的并行運(yùn)算。集群上的多進(jìn)程分布在不同的主機(jī),而單機(jī)則使用"分時(shí)復(fù)用"的方式來實(shí)現(xiàn)。下面看一個(gè)多進(jìn)程的例子

import multiprocessing as mulpr

def proc1():
    return 333*333

def proc2():
    return 444*444

p1 = mulpr.Process(target = proc1())
p2 = mulpr.Process(target = proc2())

p1.start()  # 啟動(dòng)進(jìn)程
p2.start()

p1.join()   # 結(jié)束進(jìn)程
p2.join()

多進(jìn)程和多線程的區(qū)分在哪呢?
一個(gè)程序運(yùn)行后,就是一個(gè)進(jìn)程。進(jìn)程有自己的內(nèi)存空間,存儲(chǔ)自己的運(yùn)行狀態(tài)、數(shù)據(jù)和代碼。進(jìn)程與進(jìn)程之間一般不會(huì)相互讀取數(shù)據(jù)。
但在一個(gè)進(jìn)程中,可以有多個(gè)"線程"任務(wù),處理器在多個(gè)線程之間進(jìn)行切換,從而形成并行的多線程處理。線程之間可以共享同一個(gè)進(jìn)程的內(nèi)存。

7.2 被解放的函數(shù)

1.函數(shù)作為參數(shù)

在函數(shù)式編程中,函數(shù)是第一級(jí)對(duì)象,也即是函數(shù)能像普通對(duì)象一樣使用。因此,函數(shù)也可以作為參數(shù),成為其他函數(shù)的參數(shù)。如

def square_sum(a,b):   # 準(zhǔn)備作為參數(shù)的函數(shù),求出平方和
    return a**2 + b**2

def cubic_sum(a,b):   #  準(zhǔn)備作為參數(shù)的函數(shù),求出立方和
    return a**3 + b**3

def argF_demo(f,a,b): #  f是函數(shù),a是數(shù)字,b是數(shù)字
    return f(a,b)

print(argF_demo(square_sum,3,5))
print(argF_demo(cubic_sum,3,5))

結(jié)果:
34
152

argF_demo()的第一個(gè)參數(shù)f是一個(gè)函數(shù)對(duì)象。square_sum和cubic_sum作為f傳遞給argF_demo()。

再如,圖形用戶界面中,作為參數(shù)的函數(shù)經(jīng)常起到回調(diào)(callback)的作用。當(dāng)某個(gè)事件發(fā)生時(shí)(如界面中的某個(gè)按鈕被點(diǎn)擊),回調(diào)函數(shù)就會(huì)被調(diào)用。下面是GUI回調(diào)的例子

import tkinter as tk

def callback():
    listbox.insert(tk.END,"我出現(xiàn)了,我是回調(diào)函數(shù)")
    
if __name__ == "__main__":
    master = tk.Tk()
    
    button = tk.Button(master,text = "OK",command = callback) 
    # 創(chuàng)建一個(gè)按鈕,一旦點(diǎn)擊就會(huì)使用callback函數(shù)
    button.pack()
    
    listbox = tk.Listbox(master)
    listbox.pack()
    
tk.mainloop()

Python中內(nèi)置了tkinter的圖形化功能。listbox是列表欄,就是上圖的顯示框,一旦點(diǎn)擊按鈕OK,就會(huì)在列表欄中插入"我出現(xiàn)了,我是回調(diào)函數(shù) "。

2.函數(shù)作為返回值

函數(shù)是一個(gè)對(duì)象,除了可以作為函數(shù)的參數(shù),還可以作為函數(shù)的返回值。

def line_res():
    def line(x):
        return 2*x + 1
    return line  # 返回一個(gè)函數(shù)

line1 = line_res()
print(line1(3))

結(jié)果:
7

line_res將函數(shù)line作為對(duì)象返回給line1,line1通過輸入?yún)?shù)就可以得到結(jié)果11。
從上例可以看出,函數(shù)里面再定義了函數(shù),函數(shù)內(nèi)部的函數(shù)對(duì)象也有作用域,Python中用縮進(jìn)塊來表示。如

def line_res():
    def line(x):
        return 2*x + 1
    print(line(3))  # 作用域內(nèi)

line_res()  # 打印出7
print(line(3)) # 作用域之外,將會(huì)報(bào)錯(cuò)

結(jié)果:
7
NameError: name 'line' is not defined

line()函數(shù)在line_res()的作用域之內(nèi),只能在line_res()之內(nèi)調(diào)用,當(dāng)在line_res()的作用域之外調(diào)用line()函數(shù)時(shí),將會(huì)報(bào)錯(cuò)。

3.閉包

閉包:一個(gè)函數(shù)與它的環(huán)境變量合在一起,即為閉包。
用一個(gè)例子來理解閉包和環(huán)境變量

def line_res():
    b = 15  # 這個(gè)就是line()的環(huán)境變量
    def line(x):
        return 2*x + b
    b = 5   # line()的環(huán)境變量
    return line  # 返回函數(shù)對(duì)象

line1 = line_res() # 將函數(shù)對(duì)象賦給line1
print(line1(3))

結(jié)果:
11

line()函數(shù)的引用了它作用域外的變量b,b是line()外部的變量,這個(gè)b就是line()函數(shù)的環(huán)境變量。line()函數(shù)和環(huán)境變量b合在一塊,這就是閉包。
上面代碼中,b分別在line()定義前后有兩次不同的值,最終結(jié)果是11,即line()中的b=5。因此,閉包中的環(huán)境變量是內(nèi)部函數(shù)作為對(duì)象返回時(shí),最近賦值的那個(gè)值。


在Python中,閉包(closure)是一個(gè)包含環(huán)境變量取值的函數(shù)對(duì)象。環(huán)境變量取值被復(fù)制到函數(shù)對(duì)象的__closure__屬性中。

def line_res():
    b = 15  # 這個(gè)就是line()的環(huán)境變量
    def line(x):
        return 2*x + b + a
    
    b = 5   # line()的環(huán)境變量
    a = 2   # line()的環(huán)境變量
    
    return line  # 返回函數(shù)對(duì)象

line1 = line_res()
print(line1.__closure__)  # 閉包
print(line1.__closure__[0].cell_contents) # 打印2 也即是a=2
print(line1.__closure__[1].cell_contents) # 打印5 也即是b=5

結(jié)果:
(<cell at 0x000001CC6DA83288: int object at 0x00007FFBB9B19360>,
 <cell at 0x000001CC6DDFFFD8: int object at 0x00007FFBB9B193C0>)
2
5

可以看出,閉包屬性中包含了一個(gè)元組,元組中是cell型的對(duì)象。
第一個(gè)是2,即環(huán)境變量a=2,第二個(gè)是5,即b=5。


閉包可以提高代碼的復(fù)用性。

def line1():
    return x + 1

def line2():
    return 2*x + 1

def line3():
    return 4*x + 2

上面一共有三條直線,可以看出都有一個(gè)自變量x,還有另外兩個(gè)數(shù),可以定義為a和b。則上面的直線可以統(tǒng)一寫為:a*x+b。用閉包來改寫代碼

def line_res(a,b):
    def line(x):
        return a*x + b
    return line

line1 = line_res(1,1)
line2 = line_res(2,1)
line3 = line_res(4,2)
print(line1(2),line2(2),line3(2))

結(jié)果:
3 5 10

閉包還可以起到減少函數(shù)參數(shù)的作用。

def curve(a,b,c,x):  # 定義一個(gè)二次函數(shù),需要4個(gè)參數(shù)a,b,c,x
    return a*(x**2) + b*x + c

print(curve(2,4,3,1))
print(curve(2,4,3,2))
結(jié)果:
9
19

可見每次計(jì)算一個(gè)二次函數(shù)值需要輸入很多參數(shù),盡管我們只是自變量x改變而其他參數(shù)不變。我們可以通過閉包來減少參數(shù)

def curve_closure(a,b,c):    
    def curve(x):
        return a*(x**2) + b*x + c
    return curve

curve1 = curve_closure(2,4,3)
print(curve1(1))
print(curve1(2))

結(jié)果:
9
19

這段代碼可以與上面的代碼比較,需要輸入的參數(shù)少了3個(gè)。

7.3 小女子的梳妝匣子

1.裝飾器

裝飾器是一種高級(jí)Python語法。它可以對(duì)一個(gè)函數(shù)、方法或類進(jìn)行加工。
首先我們定義兩個(gè)函數(shù),一個(gè)計(jì)算平方和,一個(gè)計(jì)算平方差。

def square_sum(a,b):
    return a**2 + b**2

def square_diff(a,b):
    return a**2 - b**2

print(square_sum(4,3))
print(square_diff(4,2))

結(jié)果:
25
12

如果我們想要顯示我們的輸入,可以改進(jìn)

def square_sum(a,b):
    print("輸入的數(shù)據(jù)",a,b)
    return a**2 + b**2

def square_diff(a,b):
    print("輸入的數(shù)據(jù)",a,b)
    return a**2 - b**2

print(square_sum(4,3))
print(square_diff(4,2))

結(jié)果:
輸入的數(shù)據(jù) 4 3
25
輸入的數(shù)據(jù) 4 2
12

可以看出來,打印輸入數(shù)據(jù)的操作語句一樣,我們可以將這個(gè)打印輸入數(shù)據(jù)的操作寫成裝飾器,然后用于函數(shù)上。

def decorator_demo(old_fun): # 定義一個(gè)裝飾器
    def new_fun(a,b):
        print("輸入的數(shù)據(jù)",a,b)
        return old_fun(a,b)
    return new_fun

@decorator_demo
def square_sum(a,b):
    return a**2 + b**2

@decorator_demo
def square_diff(a,b):
    return a**2 - b**2

print(square_sum(4,3))
print(square_diff(4,2))

結(jié)果:
輸入的數(shù)據(jù) 4 3
25
輸入的數(shù)據(jù) 4 2
12

裝飾器用def定義,它接收了一個(gè)可調(diào)用對(duì)象(如函數(shù)對(duì)象)作為輸入?yún)?shù),并且返回一個(gè)新的可調(diào)用對(duì)象,如上面的new_fun()。在new_fun()中,我們?cè)黾恿舜蛴〉墓δ埽瑫r(shí)也保留了原來old_fun的功能。

定義好裝飾器之后,我們通過"@"語法使用它。
在函數(shù)square_sum(),square_diff()之前調(diào)用裝飾器@decorator_demo,實(shí)際上就是將原來的函數(shù)square_sum(),square_diff()傳遞給decorator_demo(),然后將decorator_demo()返回的新函數(shù)對(duì)象賦給原來的函數(shù)名square_sum(),square_diff()。即如在調(diào)用square_sum(3,4)時(shí),實(shí)際發(fā)生

裝飾器起到的作用就是,讓同一個(gè)變量名指向一個(gè)新函數(shù)對(duì)象,從而達(dá)到修改函數(shù)對(duì)象的目標(biāo)。而且由于原來函數(shù)已經(jīng)定義的差不多了,只需要稍加修飾即可,仍然保留原函數(shù)的功能,這就是修飾器的作用。

我們可以再來舉一個(gè)例子,假設(shè)我們想知道函數(shù)的運(yùn)行時(shí)間。這個(gè)對(duì)于每個(gè)函數(shù)都是適用的,因此我們可以把這個(gè)功能做成裝飾器。

import time as t
def decorator(old_fun):
    def new_fun(*arg,**dict_arg):
        start = t.perf_counter()
        res = old_fun(*arg,**dict_arg)
        end = t.perf_counter()
        print("函數(shù)運(yùn)行時(shí)間為:",end - start)
        return res
    return new_fun

@decorator
def curve(a,b,c,x):
    return a*x**2 + b*x + c

print(curve(2,3,4,1))

結(jié)果:
函數(shù)運(yùn)行時(shí)間為: 4.700001227320172e-06
9

2.帶參裝飾器

裝飾器允許我們使用它時(shí),傳入其他參數(shù)。

def pre_str(pre): # 帶參的裝飾器
    def decorator(old_fun):
        def new_fun(a,b):
            print(pre,"輸入的數(shù)據(jù)是",a,b)
            return old_fun(a,b)
        return new_fun
    return decorator

#裝飾square_sum()
@pre_str(">_<")
def square_sum(a,b):
    return a**2 + b**2

#裝飾square_dif()
@pre_str("^_^")
def square_dif(a,b):
    return a**2 - b**2

print(square_sum(3,4))
print(square_dif(2,1))

結(jié)果:
>_< 輸入的數(shù)據(jù)是 3 4
25
^_^ 輸入的數(shù)據(jù)是 2 1
3

pre_str是一個(gè)帶參裝飾器,對(duì)原來裝飾器進(jìn)行封裝,返回的是一個(gè)裝飾器。當(dāng)我們使用@pre_str("_")時(shí),Python能夠發(fā)現(xiàn)這一層的封裝,并將參數(shù)傳入到裝飾器中,相當(dāng)于


根據(jù)參數(shù)的不同,帶參裝飾器會(huì)對(duì)函數(shù)進(jìn)行不同的加工,進(jìn)一步提高裝飾器的適用范圍。

3.裝飾類

裝飾器還可以裝飾類,一個(gè)裝飾器裝飾一個(gè)舊類,并返回一個(gè)新類,起到了加工類的效果。

def decorator_Class(OldClass):
    class NewClass(object):
        def __init__(self,age):
            self.total_display = 0
            self.wrapped = OldClass(age) # 將舊類的對(duì)象進(jìn)行打包
        
        def display(self):
            self.total_display += 1
            print("展示次數(shù)",self.total_display)
            self.wrapped.display()  # 調(diào)用舊類對(duì)象的功能或數(shù)學(xué)
    return NewClass # 最終返回一個(gè)新類

@decorator_Class
class Bird(object):
    def __init__(self,age):
        self.age = age
    
    def display(self):
        print("我的年齡是:",self.age,"歲")
        
eagle = Bird(5)
for i in range(3):
    eagle.display()

在裝飾器decorator_class中,我們定義了新類,在新類中的初始化方法中,用self.wrapped將舊類的對(duì)象進(jìn)行打包,并附加了新屬性total_display,用于記錄展示次數(shù)。而且我們還同時(shí)更改了display方法。通過裝飾,舊類就可以顯示調(diào)用display()的次數(shù)。

7.4 高階函數(shù)

1.lambda與map函數(shù)

能接收其他函數(shù)作為參數(shù)的函數(shù),稱為"高階函數(shù)"。像上面介紹的裝飾器,也屬于高階函數(shù)。最具有代表性的高階函數(shù)有:map()、filter()、reduce()。


在講解高階函數(shù)之前,先講解匿名函數(shù)lambda。lambda語法也可以用來定義函數(shù),只不過比較實(shí)用簡(jiǎn)短語句的函數(shù)。如

lambda_sum = lambda x,y:x+y
print(lambda_sum(3,4))

通過lambda,我們創(chuàng)建了一個(gè)匿名函數(shù)對(duì)象,它的參數(shù)是x,y,返回值是x+y,然后將它賦值給lambda_sum()。lambda_sum()的使用與正常函數(shù)一樣。lambda定義的函數(shù)適用于簡(jiǎn)短函數(shù)。


高階函數(shù)從map()開始介紹,map()的第一個(gè)參數(shù)是函數(shù)對(duì)象,它把這一個(gè)函數(shù)對(duì)象作用于后面多個(gè)元素。第二個(gè)元素之后都是循環(huán)對(duì)象。

data1 = [1,3,5,7]
res = map(lambda x:x+3,data1) # x : 4 7 8 10

map()的第一個(gè)參數(shù)是函數(shù)對(duì)象,第二個(gè)參數(shù)是可循環(huán)對(duì)象。對(duì)于data1中的每個(gè)元素,都會(huì)成為lambda函數(shù)的參數(shù),lambda函數(shù)都會(huì)調(diào)用一次。也就是說,map()接收到的函數(shù)對(duì)象參數(shù)依次作用于每一個(gè)參數(shù)。map()會(huì)返回一個(gè)迭代器。上面的代碼就相當(dāng)于如下

def generator(fun,iterator):
    for item in iterator:
        yield fun(item)
        
data1 = [1,3,5,7]
res = generator(lambda x:x+3,data1)

lambda函數(shù)也可以是多個(gè)參數(shù)的,如

def square_sum(x,y):
    return x**2 + y**2

data1 = [1,3,5,7]
data2 = [2,4,6,8]

res = map(square_sum,data1,data2)

map()函數(shù)中第一個(gè)參數(shù)是函數(shù)對(duì)象,第二和第三個(gè)是可循環(huán)對(duì)象。第二個(gè)循環(huán)對(duì)象對(duì)應(yīng)于square_sum中的x,第三個(gè)循環(huán)對(duì)象對(duì)應(yīng)于square_sum中的y。

2.filter函數(shù)

filter函數(shù)與map函數(shù)相似,但filter()只有兩個(gè)參數(shù),第一個(gè)參數(shù)也是函數(shù)對(duì)象,第二個(gè)參數(shù)都是循環(huán)對(duì)象。如果函數(shù)對(duì)象返回的是True,則該元素被放到迭代器中,即filter()函數(shù)通過調(diào)用函數(shù)來篩選數(shù)據(jù)。如

def larage100(x):
    if x > 100:
        return True
    else:
        return False

for item in filter(larage100,[1,101,202,5]):
    print(item)

結(jié)果:
101
202

import math
def is_Sqr(x):
    return math.sqrt(x) % 1 == 0  # 判斷平方根是否為整數(shù)
 
newlist = filter(is_Sqr, range(1, 101))
for item in newlist:
    print(item,end = " ")

結(jié)果:
1 4 9 16 25 36 49 64 81 100

filter()函數(shù)顧名思義,就是一個(gè)過濾器,更多的用于篩選出符合我們定義的函數(shù)對(duì)象里面的條件的數(shù)據(jù)。

3.reduce函數(shù)

reduce函數(shù)一共有三個(gè)參數(shù),第一個(gè)是函數(shù)對(duì)象,且這個(gè)函數(shù)對(duì)象能夠接收兩個(gè)參數(shù);第二個(gè)參數(shù)是序列;第三個(gè)參數(shù)是初始值,無初始值就按照序列的第一個(gè)為初始值。

from functools import reduce

res1 = reduce(lambda x,y:x+y,[1,3,5,7,9])
print(res1)

res2 = reduce(lambda x,y:x+y,["x","y","z"],"a")
print(res2)

結(jié)果:
24
axyz

從第一個(gè)res1看出,它接收兩個(gè)參數(shù)x和y,將列表中的元素進(jìn)行累進(jìn)運(yùn)算,即(((1+3)+5)+7)=16;相似的res2也是如此。


reduce通過二元運(yùn)算,將多個(gè)元素聚集成一個(gè)結(jié)果。map和reduce都是單線程的,運(yùn)行效果和循環(huán)類似,但map和reduce可以方便的移植入并行化的運(yùn)行環(huán)境下。
在并行運(yùn)算中,reduce緊接著map運(yùn)算,map將運(yùn)算結(jié)果分布在多個(gè)主機(jī)上,reduce運(yùn)算把結(jié)果收集起來。谷歌用于并行運(yùn)算的軟件架構(gòu)叫MapReduce

4.并行處理

import time
from multiprocessing import Pool
import requests

# 定義一個(gè)運(yùn)行時(shí)間的修飾器
def decorator_timer(oldFun):
    def newFun(*arg,**dictarg):
        start = time.perf_counter()
        res = oldFun(*arg,**dictarg)
        end = time.perf_counter()
        print("運(yùn)行時(shí)間為:",end - start)
        return res
    return newFun

# 訪問網(wǎng)頁任務(wù)
def download_once(i,addres="http://cnblogs.com"):
    print("第",i+1,"次訪問完成")
    r = requests.get(addres)
    return r.status_code

# 單線程,一次處理一個(gè)
@decorator_timer
def single_thread(f,times):
    print("我是單線程,每次只執(zhí)行一次")
    res = map(f,range(times))
    return list(res)

# 多線程,并行處理
@decorator_timer
def multiple_thread(f,times,process_num = 5):
    print("我是多線程,任務(wù)同時(shí)完成")
    p = Pool(process_num)
    res = p.map(f,range(times))
    return list(res)

if __name__ == "__main__":
    TOTAL = 10
    print(single_thread(download_once,TOTAL))
    print()
    print(multiple_thread(download_once,TOTAL))

結(jié)果:
我是單線程,每次只執(zhí)行一次
第 1 次訪問完成
第 2 次訪問完成
第 3 次訪問完成
第 4 次訪問完成
第 5 次訪問完成
第 6 次訪問完成
第 7 次訪問完成
第 8 次訪問完成
第 9 次訪問完成
第 10 次訪問完成
運(yùn)行時(shí)間為: 12.258385399999952
[200, 200, 200, 200, 200, 200, 200, 200, 200, 200]

我是多線程,任務(wù)同時(shí)完成
運(yùn)行時(shí)間為: 2.3248039000000063
[200, 200, 200, 200, 200, 200, 200, 200, 200, 200]

上面的download_once是單次的訪問網(wǎng)頁任務(wù),TOTAL是訪問次數(shù)需求,一共是10次。
單線程一次一次的進(jìn)行,最終獲取了10個(gè)狀態(tài)碼;
而多線程開辟了5個(gè)進(jìn)程,10次任務(wù)同時(shí)進(jìn)行,把10個(gè)任務(wù)分配給了5個(gè)工人。從運(yùn)行時(shí)間可以明顯看出,多線程的運(yùn)算更加快速。

7.5 自上而下

1.便捷式表達(dá)

Python中有幾種體現(xiàn)自上而下思維的語法,如生成器表達(dá)式、列表解析與詞典解析。

  • 生成器表達(dá)式
    生成器表達(dá)式是構(gòu)建生成器的便捷表達(dá)式。假設(shè)我們要構(gòu)建一個(gè)生成器
def gen():
    for i in range(4):
        yield i

上面代碼用生成器表達(dá)式可以改寫為

gen = (x for x in range(4))
  • 列表解析(列表生成式)
    若我們想要一個(gè)0-9每個(gè)數(shù)的平方的列表,可以這樣生成
li = []

for x in range(10):
    li.append(x**2)
    
print(li)

結(jié)果:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

更加便捷地,我們可以使用列表解析,或者說列表生成式

li = [x**2 for x in range(10)]
print(li)

結(jié)果:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

列表生成式的語法很直觀,首先說明我們需要的是什么樣的元素,再通過for加入限定條件,即哪些元素。再如生成3個(gè)隨機(jī)數(shù)列表,每個(gè)列表包含10個(gè)1到100內(nèi)的隨機(jī)整數(shù)

import random as r

ran_li1 = [r.randint(1,100) for i in range(10)]
ran_li2 = [r.randint(1,100) for i in range(10)]
ran_li3 = [r.randint(1,100) for i in range(10)]

print(ran_li1)
print(ran_li2)
print(ran_li3)

結(jié)果:
[47, 10, 21, 26, 37, 62, 28, 57, 40, 77]
[3, 7, 56, 70, 9, 17, 1, 60, 46, 96]
[59, 98, 87, 79, 55, 52, 37, 31, 84, 39]

我們還可以在列表解析里面加入if語法

x1 = [1,3,5,7]
y1 = [2,4,6,8]

li = [x**2 for x,y in zip(x1,y1) if y > 5] 
# 當(dāng)y>5時(shí),生成對(duì)應(yīng)的x的平方,再保存入列表

print(li)

結(jié)果:
[25, 49]

zip()是一個(gè)將序列打包的方法,而for i in zip()則可以看成得到序列中的每個(gè)元素,即解壓。

a = [1,2,3]
b = [4,5,6]
c = [4,5,6,7,8]

for i in zip(a,b,c):
    print(i)

結(jié)果:
(1, 4, 4)
(2, 5, 5)
(3, 6, 6)

--------------------------------------------------

x1 = [1,3,5,7]
y1 = [2,4,6,8,10]
z1 = ["a","b","c","d","e"]

for x,y,z in zip(x1,y1,z1):
    print(x,y,z)

結(jié)果:
1 2 a
3 4 b
5 6 c
7 8 d
  • 詞典解析(詞典生成式)
    類似的,詞典也可以快捷生成。與列表解析的語法類似。
d1 = {key:val for key,val in enumerate("HelloWorld")}
print(d1)

d2 = {k:v for k,v in enumerate("Python") if v in "Py"}
print(d2)
"""
enumerate是枚舉,包括索引和元素
"""
結(jié)果:
{0: 'H', 1: 'e', 2: 'l', 3: 'l', 4: 'o', 5: 'W', 6: 'o', 7: 'r', 8: 'l', 9: 'd'}
{0: 'P', 1: 'y'}

2.懶惰求值

迭代器有時(shí)候看起來像列表,但實(shí)際上迭代器的元素是實(shí)時(shí)運(yùn)算出來的,在使用元素之前,是不會(huì)占據(jù)空間;而列表在建立時(shí),就先產(chǎn)生了元素,并保存在空間內(nèi)。
迭代器的工作方式就是懶惰求值,即需要的時(shí)候再使用元素,才會(huì)計(jì)算具體的值。

import time
start1 = time.perf_counter()
for i in (x**2 for x in range(100)):
    print(i)
end1 = time.perf_counter()
print("迭代器運(yùn)行時(shí)間:",end1-start1)

結(jié)果:
0
1
4
9
……
迭代器運(yùn)行時(shí)間: 0.004747199999656004
import time
start2 = time.perf_counter()
for i in [x**2 for x in range(100)]:
    print(i)
end2 = time.perf_counter()
print("列表運(yùn)行時(shí)間:",end2 - start2)

結(jié)果:
0
1
4
9
……
列表運(yùn)行時(shí)間: 0.008195000000341679

通過上面的對(duì)比可以看出,雖然生成的結(jié)果相同,但是迭代器的運(yùn)行時(shí)間小于使用列表運(yùn)行的時(shí)間。這是因?yàn)榻⒘斜硇枰扔?jì)算產(chǎn)生元素,再保存生成入列表,再使用列表;而迭代器卻是"懶惰求值"。

map和range函數(shù)返回的都是迭代器,它們所做的也是懶惰求值,需要的時(shí)候再使用;但是若將它們轉(zhuǎn)化為列表,時(shí)間將會(huì)大大增加、如

#未轉(zhuǎn)化為列表之前
import time as t
start = t.perf_counter()
ite = range(100)
res = map(lambda x:x**2,ite)
print(res) # 返回的是一個(gè)迭代器
end = t.perf_counter()
print("運(yùn)行時(shí)間為:",end-start)

結(jié)果:
<map object at 0x0000018149C5B8D0>
運(yùn)行時(shí)間為: 0.00012900000001536682

轉(zhuǎn)化為列表

import time as t
start = t.perf_counter()
ite = range(1000)
res = map(lambda x:x**2,ite)
print(list(res))
end = t.perf_counter()
print("運(yùn)行時(shí)間為:",end-start)

結(jié)果:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401, 2500, 2601, 2704, 2809, 2916, 3025, 3136, 3249, 3364, 3481, 3600, 3721, 3844, 3969, 4096, 4225, 4356, 4489, 4624, 4761, 4900, 5041, 5184, 5329, 5476, 5625, 5776, 5929, 6084, 6241, 6400, 6561, 6724, 6889, 7056, 7225, 7396, 7569, 7744, 7921, 8100, 8281, 8464, 8649, 8836, 9025, 9216, 9409, 9604, 9801]
運(yùn)行時(shí)間為: 0.00015100000018719584

可見100次時(shí),生成為迭代器的時(shí)間只需要0.000129,而轉(zhuǎn)化為列表需要0.000151,雖然相差不大,但是已經(jīng)生成為迭代器的時(shí)間已經(jīng)小于生成為列表的時(shí)間。如果將100設(shè)置為1000,相差的時(shí)間更大。如

1000次時(shí)

生成為迭代器的時(shí)間
運(yùn)行時(shí)間為: 0.00013020000005781185

生成為列表的時(shí)間
運(yùn)行時(shí)間為: 0.0005742000003010617

可以看出,將map生成的結(jié)果轉(zhuǎn)化為列表,時(shí)間將大大增加。

再者,如果我們不需要窮盡所有數(shù)據(jù)元素,那么懶惰求值將節(jié)省很多時(shí)間,而列表生成式的方式,提前準(zhǔn)備的數(shù)據(jù)就會(huì)造成極大浪費(fèi)。如

import time as t
start1 = t.perf_counter()
for i in (x**2 for x in range(100000)):
    if i > 500:
        break
end1 = t.perf_counter()
print("迭代器運(yùn)行時(shí)間:",end1-start1)

start2 = t.perf_counter()
for i in [x**2 for x in range(100000)]:
    if i > 500:
        break
end2 = t.perf_counter()
print("列表運(yùn)行時(shí)間",end2-start2)

結(jié)果:
迭代器運(yùn)行時(shí)間: 1.700000029813964e-05
列表運(yùn)行時(shí)間 0.04208400000061374

迭代器的運(yùn)行時(shí)間為1.7*10-5,而列表運(yùn)行時(shí)間是0.0420,可見 迭代器更快。也可以看出,除了運(yùn)行更快外,懶惰求值還可以節(jié)省內(nèi)存空間。

除了map、filter函數(shù),Python中itertools包提供豐富的操作迭代器的工具。

3.itertools包

首先導(dǎo)入itertools包

from itertools import *
  • count()和cycle()可以生成無限循環(huán)的迭代器。

count(5,2):從5開始每次增加2,無限循環(huán)

# count(5,2):從5開始每次增加2,無限循環(huán)
for i in count(5,2):
    print(i)
    if i > 15:  # 到15結(jié)束,不然無限循環(huán)了
        break

結(jié)果:
5
7
9
11
13
15
17

cycle(list|str|tuple|dict):不斷重復(fù)序列中的元素

# cycle(list|str|tuple|dict):不斷重復(fù)序列中的元素
cut1 = 0
for i in cycle([1,3,"ab"]):
    cut1 += 1
    print(i)
    if cut1 == 7:  # 不斷循環(huán),直到第7次時(shí)跳出
        break

結(jié)果:
1
3
ab
1
3
ab
1
  • repeat():返回一個(gè)不斷重復(fù)元素的迭代器,也可以有次數(shù)限制的重復(fù)。
# repeat():不斷重復(fù)元素,也可以有次數(shù)限制
cut2 = 0
for i in repeat(5):
    cut2 += 1
    print(i)
    if cut2 == 7:# 不斷循環(huán),直到第7次時(shí)跳出
        break
    
for i in repeat([1,"a",1.2],4): # 重復(fù)4次該列表
    print(i)

結(jié)果:
5
5
5
5
5
5
5
[1, 'a', 1.2]
[1, 'a', 1.2]
[1, 'a', 1.2]
[1, 'a', 1.2]
  • 組合舊迭代器,生成新迭代器

chain():連接兩個(gè)及以上的迭代器

# chain():連接兩個(gè)及以上的迭代器
for i in chain([1,3,5,7],[2,4,6,8]):
    print(i)

結(jié)果:
1
3
5
7
2
4
6
8

product():返回多個(gè)迭代器的笛卡爾積,即得到元素的所有可能的組合方式,相當(dāng)于嵌套循環(huán)。

# product():返回多個(gè)迭代器的笛卡爾積,
# 即得到元素的所有可能的組合方式,相當(dāng)于嵌套循環(huán)
for i in product("abc",[1,3]): # 以元組來接收
    print(i)

for x,y in product("abc",[1,3]):# 以每個(gè)元素來接收
    print(x,y)

結(jié)果:
('a', 1)
('a', 3)
('b', 1)
('b', 3)
('c', 1)
('c', 3)
a 1
a 3
b 1
b 3
c 1
c 3

permutations("abc",2):從"abc"中挑2個(gè)元素,將所有結(jié)果排序,返回新的迭代器,且組合區(qū)分順序。

'''
permutations("abc",2)
從"abc"中挑2個(gè)元素,將所有結(jié)果排序,
返回新的迭代器,且組合區(qū)分順序
'''    
for i in permutations("abc",2):
    print(i)

結(jié)果:
('a', 'b')
('a', 'c')
('b', 'a')
('b', 'c')
('c', 'a')
('c', 'b')

combinations("abcd",3):從"abcd"中挑3個(gè)元素進(jìn)行組合,將所有結(jié)果排序,返回新迭代器且組合不區(qū)分順序,即ab和ba都是ab

'''
combinations("abcd",3)
從"abcd"中挑3個(gè)元素進(jìn)行組合,
將所有結(jié)果排序,返回新迭代器
且組合不區(qū)分順序,即ab和ba都是ab
'''
for i in combinations("abcd",3):
    print(i)

結(jié)果:
('a', 'b', 'c')
('a', 'b', 'd')
('a', 'c', 'd')
('b', 'c', 'd')

combinations_with_replacement("abcd",3):與上面類似,但是允許出現(xiàn)重復(fù)的元素,如a,a,a。

'''
combinations_with_replacement("abcd",3)
與上面類似,但是允許出現(xiàn)重復(fù)的元素,如a,a,a
'''
for i in combinations_with_replacement("abcd",3):
    print(i)

結(jié)果:
('a', 'a', 'a')
('a', 'a', 'b')
('a', 'a', 'c')
('a', 'a', 'd')
('a', 'b', 'b')
('a', 'b', 'c')
('a', 'b', 'd')
('a', 'c', 'c')
('a', 'c', 'd')
('a', 'd', 'd')
('b', 'b', 'b')
('b', 'b', 'c')
('b', 'b', 'd')
('b', 'c', 'c')
('b', 'c', 'd')
('b', 'd', 'd')
('c', 'c', 'c')
('c', 'c', 'd')
('c', 'd', 'd')
('d', 'd', 'd')
  • starmap(pow,[[1,1],(2,2),(3,3)]):將pow作用于列表中的每個(gè)序列
'''
starmap(pow,[[1,1],(2,2),(3,3)])
將pow作用于列表中的每個(gè)序列
'''
for i in starmap(pow,[[1,1],(2,2),(3,3)]):
    print(i)

結(jié)果:
1
4
27
  • takewhile(lambda x:x>2,[1,2,5,6]):只有兩個(gè)參數(shù)。當(dāng)函數(shù)返回True時(shí),收集元素到迭代器。一旦返回False,就停止。
'''
takewhile(lambda x:x>2,[1,2,5,6]),只有兩個(gè)參數(shù)
當(dāng)函數(shù)返回True時(shí),收集元素到迭代器。一旦返回False,就停止。
'''
for i in takewhile(lambda x:x<6,[1,2,5,6]):
    print(i)

結(jié)果:
1
2
5

dropwhile(lambda x:x<3,[1,2,3,2,1,8]):當(dāng)函數(shù)返回False時(shí),跳過并記錄元素。一旦返回True,則開始收集剩下的所有元素到迭代器。

'''
dropwhile(lambda x:x<3,[1,2,3,2,1,8])
當(dāng)函數(shù)返回False時(shí),跳過并記錄元素
一旦返回True,則開始收集剩下的所有元素到迭代器。
'''
for i in dropwhile(lambda x:x<3,[1,2,3,2,1,8]):
    print(i)

結(jié)果:
3
2
1
8

groupby():能將一個(gè)key函數(shù)作用于原迭代器的各個(gè)元素,從而獲得各個(gè)函數(shù)的鍵值。根據(jù)key()函數(shù)的結(jié)果,來對(duì)原來迭代器的元素進(jìn)行分組。

'''
groupby():
能將一個(gè)key函數(shù)作用于原迭代器的各個(gè)元素,從而獲得各個(gè)函數(shù)的鍵值。
根據(jù)key()函數(shù)的結(jié)果,來對(duì)原來迭代器的元素進(jìn)行分組。
'''
def height_keyFun(height):
    if height > 180:
        return "Tall"
    elif height < 160:
        return "Short"
    else:
        return "Middle"

People = [191,160,158,172,185,157,179]
People = sorted(People,key = height_keyFun) # 按照height_keyFun進(jìn)行排序,
                                           # 讓同組元素先在位置上靠攏。
print(People)

for x,y in groupby(People,key = height_keyFun):
    print(x) # x是鍵
    print(list(y)) # y是一個(gè)迭代器,相對(duì)應(yīng)于鍵的值,轉(zhuǎn)化為列表才可以顯示

結(jié)果:
[160, 172, 179, 158, 157, 191, 185]
Middle
[160, 172, 179]
Short
[158, 157]
Tall
[191, 185]
  • 方便迭代器構(gòu)建的工具
    compress(["a",1,9,7],[1,0,0,1]):根據(jù)真假情況,選擇保留序列中的元素。True保存,False不保存
'''
compress(["a",1,9,7],[1,0,0,1])
根據(jù)真假情況,選擇保留序列中的元素。True保存,False不保存
'''
for i in compress(["a",1,9,7],[1,0,0,1]):
    print(i)

結(jié)果:
a
7

islice()與slice相似,只不過返回的是一個(gè)迭代器

'''
slice(start,stop,step) 切片器
start -- 起始位置
stop -- 結(jié)束位置
step -- 步長(zhǎng)

islice()與slice相似,只不過返回的是一個(gè)迭代器
'''
li = [1,2,3,"a","z"]
print("切片slice1",li[slice(1,4,1)])
print("切片slice2",li[slice(3)])
print("我是slice,返回的是函數(shù)對(duì)象",slice(3))

print("我是islice,返回的是迭代器",islice(li,4))
for i in islice(li,5):
    print(i,end = " ")

結(jié)果:
切片slice1 [2, 3, 'a']
切片slice2 [1, 2, 3]
我是slice,返回的是函數(shù)對(duì)象 slice(None, 3, None)
我是islice,返回的是迭代器 <itertools.islice object at 0x000001814A824958>
1 2 3 a z 

zip_longest()與zip()相似,但是返回的是一個(gè)迭代器,且按照的是最長(zhǎng)的序列來組合的

'''
zip()在上述中講過,是按照最短序列來組合的
zip_longest()與其相似,但是返回的是一個(gè)迭代器,且按照的是最長(zhǎng)的序列來組合的
'''
a = [1,"abc",8,"x","yz"]
b = [9,"ok","Python",777]

print("我是zip(),返回的是一個(gè)函數(shù)對(duì)象,是按照最短序列來組合的",zip(a,b))
for i in zip(a,b):
    print(i)

print()

print("我是zip_longest(),返回的是一個(gè)迭代器,\
      且按照的是最長(zhǎng)的序列來組合的,沒有就給None",zip_longest(a,b))
for i in zip_longest(a,b):
    print(i)

結(jié)果:
我是zip(),返回的是一個(gè)函數(shù)對(duì)象,是按照最短序列來組合的
 <zip object at 0x0000018149C7D9C8>
(1, 9)
('abc', 'ok')
(8, 'Python')
('x', 777)

我是zip_longest(),返回的是一個(gè)迭代器,且按照的是最長(zhǎng)的序列來組合的,沒有就給None 
<itertools.zip_longest object at 0x000001814A824958>
(1, 9)
('abc', 'ok')
(8, 'Python')
('x', 777)
('yz', None)

以上是itertools包的各種功能。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容