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

7.1 又見函數(shù)

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

前面介紹了面向進程和面向?qū)ο蟮木幊蹋@一章講解面向函數(shù)的編程即函數(shù)式編程视译。函數(shù)式編程的本質(zhì)在于封裝,它以函數(shù)為中心進行代碼封裝,函數(shù)在面向進程中涉及過私沮,它有參數(shù)和返回值,起到輸入和輸出數(shù)據(jù)的功能和橙。
函數(shù)式編程強調(diào)了函數(shù)的純粹性仔燕,一個純函數(shù)是沒有副作用的,即一個函數(shù)不會影響其他函數(shù)魔招。我們在之前的可變對象中演示過晰搀,在函數(shù)內(nèi)部改變列表里面的對象,會影響函數(shù)外部的列表元素办斑,其他調(diào)用該列表的函數(shù)也會產(chǎn)生相應的影響外恕,這就產(chǎn)生了副作用。所以乡翅,為了是函數(shù)純粹鳞疲,我們在函數(shù)內(nèi)部使用的都是不可變更的變量。

由于Python中存在可變對象蠕蚜,因此只能盡量避免副作用尚洽。
函數(shù)式編程由于函數(shù)相互獨立,因此不用擔心函數(shù)調(diào)用對其他函數(shù)的影響波势,使用起來更加簡單翎朱。
而且涉枫,純函數(shù)也方便進行并行化運算喘漏。

在進行并行化編程時,我們經(jīng)常擔心不同進程之間相互干擾的問題士修。當多個進程或者線程同時修改一個變量時凛忿,進程或線程的先后順序會影響最終結果澈灼,比如

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結束
th2.join()   # 線程2結束 

print(x)

結果:
20

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

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

print(x)

結果:
30

global是聲明全局變量的關鍵字,函數(shù)對全局變量的修改能被其他函數(shù)看見,因此造成了副作用叁熔。如果并行地執(zhí)行上面兩個函數(shù)委乌,執(zhí)行順序是不確定的,結果也不一樣荣回。比如先執(zhí)行double遭贸,再執(zhí)行plus_ten,得到的結果是20心软;若先執(zhí)行plus_ten壕吹,再執(zhí)行double,得到的結果是30删铃。這被稱為竟跑條件耳贬,是并行編程中需要盡力避免的。

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

函數(shù)式編程的思路是自上而下的风题,先提出一個大問題,在最高層定義一個函數(shù)來解決這個大問題嫉父,在函數(shù)的內(nèi)部沛硅,再用其他函數(shù)來解決小問題。比如绕辖,我們要解決"如何把大象放進冰箱"的問題摇肌,首先我們定義一個函數(shù)"把大象放進冰箱"來解決大問題,然后在"把大象放進冰箱"里面仪际,有三步函數(shù)"打開冰箱門"围小、"放入大象"、"關上冰箱"树碱,如果需要繼續(xù)細化肯适,則在這三步函數(shù)內(nèi)部繼續(xù)調(diào)用其他函數(shù)。

2.并行運算

并行運算是指多條指令同時執(zhí)行成榜。對應的串行運算指的是同一時間只能執(zhí)行一條指令框舔。

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

在一臺單機上,往往運行著多個程序纬凤,即進程福贞。如用瀏覽器上網(wǎng)時,用網(wǎng)易云聽音樂停士,計算機就同時運行兩個進程肚医。這就是單機"分時復用"的方式,把運算能力分給多個進程向瓷。
也就是說肠套,集群和單機都實現(xiàn)了多個進程的并行運算。集群上的多進程分布在不同的主機猖任,而單機則使用"分時復用"的方式來實現(xià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()  # 啟動進程
p2.start()

p1.join()   # 結束進程
p2.join()

多進程和多線程的區(qū)分在哪呢?
一個程序運行后朱躺,就是一個進程刁赖。進程有自己的內(nèi)存空間,存儲自己的運行狀態(tài)长搀、數(shù)據(jù)和代碼宇弛。進程與進程之間一般不會相互讀取數(shù)據(jù)。
但在一個進程中源请,可以有多個"線程"任務枪芒,處理器在多個線程之間進行切換,從而形成并行的多線程處理谁尸。線程之間可以共享同一個進程的內(nèi)存舅踪。

7.2 被解放的函數(shù)

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

在函數(shù)式編程中,函數(shù)是第一級對象良蛮,也即是函數(shù)能像普通對象一樣使用抽碌。因此,函數(shù)也可以作為參數(shù)决瞳,成為其他函數(shù)的參數(shù)货徙。如

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

def cubic_sum(a,b):   #  準備作為參數(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))

結果:
34
152

argF_demo()的第一個參數(shù)f是一個函數(shù)對象胸囱。square_sum和cubic_sum作為f傳遞給argF_demo()祷舀。

再如,圖形用戶界面中,作為參數(shù)的函數(shù)經(jīng)常起到回調(diào)(callback)的作用裳扯。當某個事件發(fā)生時(如界面中的某個按鈕被點擊)抛丽,回調(diào)函數(shù)就會被調(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)建一個按鈕饰豺,一旦點擊就會使用callback函數(shù)
    button.pack()
    
    listbox = tk.Listbox(master)
    listbox.pack()
    
tk.mainloop()

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

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

函數(shù)是一個對象垒探,除了可以作為函數(shù)的參數(shù),還可以作為函數(shù)的返回值怠李。

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

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

結果:
7

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

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

line_res()  # 打印出7
print(line(3)) # 作用域之外惕鼓,將會報錯

結果:
7
NameError: name 'line' is not defined

line()函數(shù)在line_res()的作用域之內(nèi),只能在line_res()之內(nèi)調(diào)用唐础,當在line_res()的作用域之外調(diào)用line()函數(shù)時箱歧,將會報錯。

3.閉包

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

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

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

結果:
11

line()函數(shù)的引用了它作用域外的變量b,b是line()外部的變量汞幢,這個b就是line()函數(shù)的環(huán)境變量。line()函數(shù)和環(huán)境變量b合在一塊微谓,這就是閉包森篷。
上面代碼中,b分別在line()定義前后有兩次不同的值豺型,最終結果是11仲智,即line()中的b=5。因此姻氨,閉包中的環(huán)境變量是內(nèi)部函數(shù)作為對象返回時钓辆,最近賦值的那個值。


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

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

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

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

可以看出,閉包屬性中包含了一個元組似嗤,元組中是cell型的對象啸臀。
第一個是2,即環(huán)境變量a=2烁落,第二個是5乘粒,即b=5。


閉包可以提高代碼的復用性伤塌。

def line1():
    return x + 1

def line2():
    return 2*x + 1

def line3():
    return 4*x + 2

上面一共有三條直線灯萍,可以看出都有一個自變量x,還有另外兩個數(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))

結果:
3 5 10

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

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

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

可見每次計算一個二次函數(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))

結果:
9
19

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

7.3 小女子的梳妝匣子

1.裝飾器

裝飾器是一種高級Python語法巾陕。它可以對一個函數(shù)讨跟、方法或類進行加工。
首先我們定義兩個函數(shù)鄙煤,一個計算平方和晾匠,一個計算平方差。

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))

結果:
25
12

如果我們想要顯示我們的輸入梯刚,可以改進

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))

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

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

def decorator_demo(old_fun): # 定義一個裝飾器
    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))

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

裝飾器用def定義,它接收了一個可調(diào)用對象(如函數(shù)對象)作為輸入?yún)?shù)锥腻,并且返回一個新的可調(diào)用對象嗦董,如上面的new_fun()。在new_fun()中瘦黑,我們增加了打印的功能京革,同時也保留了原來old_fun的功能奇唤。

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

裝飾器起到的作用就是,讓同一個變量名指向一個新函數(shù)對象葛虐,從而達到修改函數(shù)對象的目標胎源。而且由于原來函數(shù)已經(jīng)定義的差不多了,只需要稍加修飾即可屿脐,仍然保留原函數(shù)的功能涕蚤,這就是修飾器的作用。

我們可以再來舉一個例子的诵,假設我們想知道函數(shù)的運行時間万栅。這個對于每個函數(shù)都是適用的,因此我們可以把這個功能做成裝飾器西疤。

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ù)運行時間為:",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))

結果:
函數(shù)運行時間為: 4.700001227320172e-06
9

2.帶參裝飾器

裝飾器允許我們使用它時烦粒,傳入其他參數(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))

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

pre_str是一個帶參裝飾器代赁,對原來裝飾器進行封裝扰她,返回的是一個裝飾器。當我們使用@pre_str("_")時芭碍,Python能夠發(fā)現(xiàn)這一層的封裝徒役,并將參數(shù)傳入到裝飾器中,相當于


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

3.裝飾類

裝飾器還可以裝飾類瞻讽,一個裝飾器裝飾一個舊類狐蜕,并返回一個新類,起到了加工類的效果卸夕。

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

@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將舊類的對象進行打包贡羔,并附加了新屬性total_display,用于記錄展示次數(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ù)。如

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

通過lambda杨帽,我們創(chuàng)建了一個匿名函數(shù)對象漓穿,它的參數(shù)是x,y,返回值是x+y注盈,然后將它賦值給lambda_sum()晃危。lambda_sum()的使用與正常函數(shù)一樣。lambda定義的函數(shù)適用于簡短函數(shù)当凡。


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

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

map()的第一個參數(shù)是函數(shù)對象,第二個參數(shù)是可循環(huán)對象朴则。對于data1中的每個元素权纤,都會成為lambda函數(shù)的參數(shù),lambda函數(shù)都會調(diào)用一次乌妒。也就是說汹想,map()接收到的函數(shù)對象參數(shù)依次作用于每一個參數(shù)。map()會返回一個迭代器撤蚊。上面的代碼就相當于如下

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ù)也可以是多個參數(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ù)中第一個參數(shù)是函數(shù)對象,第二和第三個是可循環(huán)對象侦啸。第二個循環(huán)對象對應于square_sum中的x槽唾,第三個循環(huán)對象對應于square_sum中的y丧枪。

2.filter函數(shù)

filter函數(shù)與map函數(shù)相似,但filter()只有兩個參數(shù)庞萍,第一個參數(shù)也是函數(shù)對象拧烦,第二個參數(shù)都是循環(huán)對象。如果函數(shù)對象返回的是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)

結果:
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 = " ")

結果:
1 4 9 16 25 36 49 64 81 100

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

3.reduce函數(shù)

reduce函數(shù)一共有三個參數(shù)践付,第一個是函數(shù)對象秦士,且這個函數(shù)對象能夠接收兩個參數(shù);第二個參數(shù)是序列永高;第三個參數(shù)是初始值隧土,無初始值就按照序列的第一個為初始值。

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)

結果:
24
axyz

從第一個res1看出命爬,它接收兩個參數(shù)x和y曹傀,將列表中的元素進行累進運算,即(((1+3)+5)+7)=16饲宛;相似的res2也是如此皆愉。


reduce通過二元運算,將多個元素聚集成一個結果艇抠。map和reduce都是單線程的幕庐,運行效果和循環(huán)類似,但map和reduce可以方便的移植入并行化的運行環(huán)境下家淤。
在并行運算中异剥,reduce緊接著map運算,map將運算結果分布在多個主機上絮重,reduce運算把結果收集起來冤寿。谷歌用于并行運算的軟件架構叫MapReduce

4.并行處理

import time
from multiprocessing import Pool
import requests

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

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

# 單線程,一次處理一個
@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("我是多線程,任務同時完成")
    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))

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

我是多線程号杠,任務同時完成
運行時間為: 2.3248039000000063
[200, 200, 200, 200, 200, 200, 200, 200, 200, 200]

上面的download_once是單次的訪問網(wǎng)頁任務,TOTAL是訪問次數(shù)需求丰歌,一共是10次究流。
單線程一次一次的進行辣吃,最終獲取了10個狀態(tài)碼;
而多線程開辟了5個進程芬探,10次任務同時進行,把10個任務分配給了5個工人厘惦。從運行時間可以明顯看出偷仿,多線程的運算更加快速。

7.5 自上而下

1.便捷式表達

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

  • 生成器表達式
    生成器表達式是構建生成器的便捷表達式羡玛。假設我們要構建一個生成器
def gen():
    for i in range(4):
        yield i

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

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

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

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

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

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

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

列表生成式的語法很直觀薄榛,首先說明我們需要的是什么樣的元素,再通過for加入限定條件让歼,即哪些元素敞恋。再如生成3個隨機數(shù)列表,每個列表包含10個1到100內(nèi)的隨機整數(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)

結果:
[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] 
# 當y>5時谋右,生成對應的x的平方硬猫,再保存入列表

print(li)

結果:
[25, 49]

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

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

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

結果:
(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)

結果:
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是枚舉,包括索引和元素
"""
結果:
{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.懶惰求值

迭代器有時候看起來像列表呢岗,但實際上迭代器的元素是實時運算出來的冕香,在使用元素之前,是不會占據(jù)空間后豫;而列表在建立時悉尾,就先產(chǎn)生了元素,并保存在空間內(nèi)挫酿。
迭代器的工作方式就是懶惰求值构眯,即需要的時候再使用元素,才會計算具體的值早龟。

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

結果:
0
1
4
9
……
迭代器運行時間: 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("列表運行時間:",end2 - start2)

結果:
0
1
4
9
……
列表運行時間: 0.008195000000341679

通過上面的對比可以看出惫霸,雖然生成的結果相同猫缭,但是迭代器的運行時間小于使用列表運行的時間。這是因為建立列表需要先計算產(chǎn)生元素壹店,再保存生成入列表猜丹,再使用列表;而迭代器卻是"懶惰求值"硅卢。

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

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

結果:
<map object at 0x0000018149C5B8D0>
運行時間為: 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("運行時間為:",end-start)

結果:
[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]
運行時間為: 0.00015100000018719584

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

1000次時

生成為迭代器的時間
運行時間為: 0.00013020000005781185

生成為列表的時間
運行時間為: 0.0005742000003010617

可以看出碾褂,將map生成的結果轉(zhuǎn)化為列表,時間將大大增加历葛。

再者正塌,如果我們不需要窮盡所有數(shù)據(jù)元素,那么懶惰求值將節(jié)省很多時間恤溶,而列表生成式的方式乓诽,提前準備的數(shù)據(jù)就會造成極大浪費。如

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("迭代器運行時間:",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("列表運行時間",end2-start2)

結果:
迭代器運行時間: 1.700000029813964e-05
列表運行時間 0.04208400000061374

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

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

3.itertools包

首先導入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結束,不然無限循環(huán)了
        break

結果:
5
7
9
11
13
15
17

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

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

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

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

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

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

結果:
1
3
5
7
2
4
6
8

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

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

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

結果:
('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個元素撑帖,將所有結果排序,返回新的迭代器澳眷,且組合區(qū)分順序胡嘿。

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

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

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

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

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

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

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

結果:
('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作用于列表中的每個序列
'''
starmap(pow,[[1,1],(2,2),(3,3)])
將pow作用于列表中的每個序列
'''
for i in starmap(pow,[[1,1],(2,2),(3,3)]):
    print(i)

結果:
1
4
27
  • takewhile(lambda x:x>2,[1,2,5,6]):只有兩個參數(shù)。當函數(shù)返回True時泰演,收集元素到迭代器呻拌。一旦返回False,就停止睦焕。
'''
takewhile(lambda x:x>2,[1,2,5,6]),只有兩個參數(shù)
當函數(shù)返回True時藐握,收集元素到迭代器。一旦返回False垃喊,就停止猾普。
'''
for i in takewhile(lambda x:x<6,[1,2,5,6]):
    print(i)

結果:
1
2
5

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

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

結果:
3
2
1
8

groupby():能將一個key函數(shù)作用于原迭代器的各個元素,從而獲得各個函數(shù)的鍵值需频。根據(jù)key()函數(shù)的結果漓帅,來對原來迭代器的元素進行分組。

'''
groupby():
能將一個key函數(shù)作用于原迭代器的各個元素炕泳,從而獲得各個函數(shù)的鍵值纵诞。
根據(jù)key()函數(shù)的結果,來對原來迭代器的元素進行分組培遵。
'''
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進行排序浙芙,
                                           # 讓同組元素先在位置上靠攏。
print(People)

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

結果:
[160, 172, 179, 158, 157, 191, 185]
Middle
[160, 172, 179]
Short
[158, 157]
Tall
[191, 185]
  • 方便迭代器構建的工具
    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)

結果:
a
7

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

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

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

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

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

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

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

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

print()

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

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

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

以上是itertools包的各種功能。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末托启,一起剝皮案震驚了整個濱河市宅倒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌屯耸,老刑警劉巖拐迁,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異疗绣,居然都是意外死亡线召,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門多矮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缓淹,“玉大人哈打,你說我怎么就攤上這事⊙逗” “怎么了料仗?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長伏蚊。 經(jīng)常有香客問我立轧,道長,這世上最難降的妖魔是什么躏吊? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任氛改,我火速辦了婚禮,結果婚禮上比伏,老公的妹妹穿的比我還像新娘胜卤。我一直安慰自己,他們只是感情好赁项,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布瑰艘。 她就那樣靜靜地躺著,像睡著了一般肤舞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上均蜜,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天李剖,我揣著相機與錄音,去河邊找鬼囤耳。 笑死篙顺,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的充择。 我是一名探鬼主播德玫,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼椎麦!你這毒婦竟也來了宰僧?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤观挎,失蹤者是張志新(化名)和其女友劉穎琴儿,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嘁捷,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡造成,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了雄嚣。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晒屎。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鼓鲁,到底是詐尸還是另有隱情蕴轨,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布坐桩,位于F島的核電站尺棋,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏绵跷。R本人自食惡果不足惜膘螟,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望碾局。 院中可真熱鬧荆残,春花似錦、人聲如沸净当。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽像啼。三九已至俘闯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間忽冻,已是汗流浹背真朗。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留僧诚,地道東北人遮婶。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像湖笨,于是被迫代替她去往敵國和親旗扑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容