Python技巧匿名函數(shù)、回調(diào)函數(shù)和高階函數(shù)
1、定義匿名或內(nèi)聯(lián)函數(shù)
如果我們想提供一個短小的回調(diào)函數(shù)供sort()
這樣的函數(shù)用,但不想用def
這樣的語句編寫一個單行的函數(shù),我們可以借助lambda表達式來編寫“內(nèi)聯(lián)”式的函數(shù)。
如下圖所示:
add = lambda x, y: x + y print(add(2, 3)) # 5 print(add("hello", "world!")) # helloworld
可以看到,這里用到的lambda
表達式和普通的函數(shù)定義有著相同的功能。
lambda表達式常常做為回調(diào)函數(shù)使用,有在排序以及對數(shù)據(jù)進行預處理時有許多用武之地,
如下所示:
names = [ 'David Beazley', 'Brian Jones', 'Reymond Hettinger', 'Ned Batchelder'] sorted_names = sorted(names, key=lambda name: name.split()[-1].lower()) print(sorted_names) # ['Ned Batchelder', 'David Beazley', 'Reymond Hettinger', 'Brian Jones']
lambda
雖然靈活易用,但是局限性也大,相當于其函數(shù)體中只能定義一條語句,不能執(zhí)行條件分支、迭代、異常處理等操作。
2、在匿名函數(shù)中綁定變量的值
現(xiàn)在我們想在匿名函數(shù)定義時完成對特定變量(一般是常量)的綁定,以便后期使用。
如果我們這樣寫:
x = 10 a = lambda y: x + y x = 20 b = lambda y: x + y
然后計算a(10)
和b(10)。你可能希望結(jié)果是20和30,然而實際程序的運行結(jié)果會出人意料:結(jié)果是30和30。
這個問題的關(guān)鍵在于lambda
表達式中的x是個自由變量(未綁定到本地作用域的變量),在運行時綁定而不是定義的時候綁定(其實普通函數(shù)中使用自由變量同理),而這里執(zhí)行a(10)的時候x已經(jīng)變成了20,故最終a(10)的值為30。如果希望匿名函數(shù)在定義的時候綁定變量,而之后綁定值不再變化,那我們可以將想要綁定的變量做為默認參數(shù),
如下所示:
x = 10 a = lambda y, x=x: x + y x = 20 b = lambda y, x=x: x + y print(a(10)) # 20 print(b(10)) # 30
上面我們提到的這個陷阱常見于一些對lambda
函數(shù)過于“聰明”的應(yīng)用中。比如我們想用列表推導式來創(chuàng)建一個列表的lambda
函數(shù)并期望lambda
函數(shù)能記住迭代變量。
funcs = [lambda x: x + n for n in range(5)] for f in funcs: print(f(0)) # 4 # 4 # 4 # 4 # 4
可以看到與我們期望的不同,所有lambda
函數(shù)都認為n是4。
如上所述,我們修改成以下代碼即可:
funcs = [lambda x, n=n: x + n for n in range(5)] for f in funcs: print(f(0)) # 0 # 1 # 2 # 3 # 4
3、讓帶有n個參數(shù)的可調(diào)用對象以較少的參數(shù)調(diào)用
假設(shè)我們現(xiàn)在有個n個參數(shù)的函數(shù)做為回調(diào)函數(shù)使用,但這個函數(shù)需要的參數(shù)過多,而回調(diào)函數(shù)只能有個參數(shù)。如果需要減少函數(shù)的參數(shù)數(shù)量,需要時用functools
包。functools
這個包內(nèi)的函數(shù)全部為高階函數(shù)。高階函數(shù)即參數(shù)或(和)返回值為其他函數(shù)的函數(shù)。通常來說,此模塊的功能適用于所有可調(diào)用對象。
比如functools.partial()就是一個高階函數(shù), 它的原型如下:
functools.partial(func, /, *args, **keywords)
它接受一個func函數(shù)做為參數(shù),并且它會返回一個新的newfunc
對象,這個新的newfunc
對象已經(jīng)附帶了位置參數(shù)args和關(guān)鍵字參數(shù)keywords,之后在調(diào)用newfunc
時就可以不用再傳已經(jīng)設(shè)定好的參數(shù)了。
如下所示:
def spam(a, b, c, d): print(a, b, c, d) from functools import partial s1 = partial(spam, 1) # 設(shè)定好a = 1(如果沒指定參數(shù)名,默認按順序設(shè)定) s1(2, 3, 4) # 1 2 3 4 s2 = partial(spam, d=42) # 設(shè)定好d為42 s2(1, 2, 3) # 1 2 3 42 s3 = partial(spam, 1, 2, d=42) #設(shè)定好a = 1, b = 2, d = 42 s3(3) # 1 2 3 42
上面提到的技術(shù)常常用于將不兼容的代碼“粘”起來,尤其是在你調(diào)用別人的輪子,而別人寫好的函數(shù)不能修改的時候。比如我們有以下一組元組表示的點的坐標:
points = [(1, 2), (3, 4), (5, 6), (7, 8)]
有已知的一個distance()
函數(shù)可供使用,假設(shè)這是別人造的輪子不能修改。
import math def distance(p1, p2): x1, y1 = p1 x2, y2 = p2 return math.hypot(x2 - x1, y2 - y1)
接下來我們想根據(jù)列表中這些點到一個定點pt=(4, 3)的距離來排序。我們知道列表的sort()方法
可以接受一個key參數(shù)(傳入一個回調(diào)函數(shù))來做自定義的排序處理。但傳入的回調(diào)函數(shù)只能有一個參數(shù),這里的distance()
函數(shù)有兩個參數(shù),顯然不能直接做為回調(diào)函數(shù)使用。
下面我們用partical()來解決這個問題:
pt = (4, 3) points.sort(key=partial(distance, pt)) # 先指定好一個參數(shù)為pt=(4,3) print(points) # [(3, 4), (1, 2), (5, 6), (7, 8)]
可以看到,排序正確運行。還有一種方法要臃腫些,那就是將回調(diào)函數(shù)distance嵌套進另一個只有一個參數(shù)的lambda
函數(shù)中:
pt = (4, 3) points.sort(key=lambda p: distance(p, pt)) print(points) # [(3, 4), (1, 2), (5, 6), (7, 8)]
這種方法一來臃腫,二來仍然存在我們上面提到過的一個毛病,如果我們定義回調(diào)函數(shù)后對pt有所修改,就會發(fā)生我們上面所說的不愉快的事情:
pt = (4, 3) func_key = lambda p: distance(p ,pt) pt = (0, 0) # 像這樣,后面pt變了就GG points.sort(key=func_key) print(points) # [(1, 2), (3, 4), (5, 6), (7, 8)]
可以看到,最終排序的結(jié)果由于后面pt的改變而變得完全不同了。所以我們還是建議大家采用使用functools.partial()
函數(shù)來達成目的。
下面這段代碼也是用partial()函數(shù)來調(diào)整函數(shù)簽名的例子。這段代碼利用multiprocessing
模塊以異步方式計算某個結(jié)果,然后用一個回調(diào)函數(shù)來打印該結(jié)果,該回調(diào)函數(shù)可接受這個結(jié)果和一個事先指定好的日志參數(shù)。
# result:回調(diào)函數(shù)本身該接受的參數(shù), log是我想使其擴展的參數(shù) def output_result(result, log=None): if log is not None: log.debug('Got: %r', result) def add(x, y): return x + y if __name__ == '__main__': import logging from multiprocessing import Pool from functools import partial logging.basicConfig(level=logging.DEBUG) log = logging.getLogger('test') p = Pool() p.apply_async(add, (3, 4), callback=partial(output_result, log=log)) p.close() p.join() # DEBUG:test:Got: 7
下面這個例子則源于一個在編寫網(wǎng)絡(luò)服務(wù)器中所面對的問題。比如我們在socketServer
模塊的基礎(chǔ)上,
編寫了下面這個簡單的echo服務(wù)程序:
from socketserver import StreamRequestHandler, TCPServer class EchoHandler(StreamRequestHandler): def handle(self): for line in self.rfile: self.wfile.write(b'GoT:' + line) serv = TCPServer(('', 15000), EchoHandler) serv.serve_forever()
現(xiàn)在,我們想在EchoHandler
類中增加一個__init__()
方法,它接受額外的一個配置參數(shù),用于事先指定ack。即:
class EchoHandler(StreamRequestHandler): def __init__(self, *args, ack, **kwargs): self.ack = ack super().__init__(*args, **kwargs) def handle(self) -> None: for line in self.rfile: self.wfile.write(self.ack + line)
假如我們就這樣直接改動,就會發(fā)現(xiàn)后面會提示__init__()函數(shù)缺少keyword-only
參數(shù)ack(這里調(diào)用EchoHandler()
初始化對象的時候會隱式調(diào)用__init__()函數(shù))。 我們用partical()也能輕松解決這個問題,即為EchoHandler()
事先提供好ack參數(shù)。
from functools import partial serv = TCPServer(('', 15000), partial(EchoHandler, ack=b'RECEIVED')) serv.serve_forever()
4、在回調(diào)函數(shù)中攜帶額外的狀態(tài)
我們知道,我們調(diào)用回調(diào)函數(shù)后,就會跳轉(zhuǎn)到一個全新的環(huán)境,此時會丟失我們原本的環(huán)境狀態(tài)。接下來我們討論如何在回調(diào)函數(shù)中攜帶額外的狀態(tài)以便在回調(diào)函數(shù)內(nèi)部使用。
因為對回調(diào)函數(shù)的應(yīng)用在與異步處理相關(guān)的庫和框架中比較常見,我們下面的例子也多和異步處理相關(guān)?,F(xiàn)在我們定義了一個異步處理函數(shù),它會調(diào)用一個回調(diào)函數(shù)。
def apply_async(func, args, *, callback): # 計算結(jié)果 result = func(*args) # 將結(jié)果傳給回調(diào)函數(shù) callback(result)
下面展示上述代碼如何使用:
# 要回調(diào)的函數(shù) def print_result(result): print("Got: ", result) def add(x, y): return x + y apply_async(add, (2, 3), callback=print_result) # Got: 5 apply_async(add, ('hello', 'world'), callback=print_result) # Got: helloworld
現(xiàn)在我們希望回調(diào)函數(shù)print_reuslt()
能夠接受更多的參數(shù),比如其他變量或者環(huán)境狀態(tài)信息。比如我們想讓print_result()函數(shù)每次的打印信息都包括一個序列號,以表示這是第幾次被調(diào)用,如[1] ...、[2] ...這樣。首先我們想到,可以用額外的參數(shù)在回調(diào)函數(shù)中攜帶狀態(tài),然后用partial()來處理參數(shù)個數(shù)問題:
class SequenceNo: def __init__(self) -> None: self.sequence = 0 def handler(result, seq): seq.sequence += 1 print("[{}] Got: {}".format(seq.sequence, result)) seq = SequenceNo() from functools import partial apply_async(add, (2, 3), callback=partial(handler, seq=seq)) # [1] Got: 5 apply_async(add, ('hello', 'world'), callback=partial(handler, seq=seq)) # [2] Got: helloworld
看起來整個代碼有點松散繁瑣,我們有沒有什么更簡潔緊湊的方法能夠處理這個問題呢?答案是直接使用和其他類綁定的方法(bound-method)。比如面這段代碼就將print_result
做為一個類的方法,這個類保存了計數(shù)用的ack序列號,每當調(diào)用print_reuslt()
打印一個結(jié)果時就遞增1:
class ResultHandler: def __init__(self) -> None: self.sequence = 0 def handler(self, result): self.sequence += 1 print("[{}] Got: {}".format(self.sequence, result)) apply_async(add, (2, 3), callback=r.handler) # [1] Got: 5 apply_async(add, ('hello', 'world'), callback=r.handler) # [2] Got: helloworld
還有一種實現(xiàn)方法是使用閉包,這種方法和使用類綁定方法相似。但閉包更簡潔優(yōu)雅,運行速度也更快:
def make_handler(): sequence = 0 def handler(result): nonlocal sequence # 在閉包中編寫函數(shù)來修改內(nèi)層變量,需要用nonlocal聲明 sequence += 1 print("[{}] Got: {}".format(sequence, result)) return handler handler = make_handler() apply_async(add, (2, 3), callback=handler) # [1] Got: 5 apply_async(add, ('hello', 'world'), callback=handler) # [2] Got: helloworld
最后一種方法,則是利用協(xié)程(coroutine)來完成同樣的任務(wù):
def make_handler_cor(): sequence = 0 while True: result = yield sequence += 1 print("[{}] Got: {}".format(sequence, result)) handler = make_handler_cor() next(handler) # 切記在yield之前一定要加這一句 apply_async(add, (2, 3), callback=handler.send) #對于協(xié)程來說,可以使用它的send()方法來做為回調(diào)函數(shù) # [1] Got: 5 apply_async(add, ('hello', 'world'), callback=handler.send) # [2] Got: helloworld
到此這篇關(guān)于Python技巧匿名函數(shù)、回調(diào)函數(shù)和高階函數(shù) 的文章就介紹到這了,更多相關(guān)Python匿名函數(shù)、回調(diào)函數(shù)和高階函數(shù) 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python+mysql實現(xiàn)個人論文管理系統(tǒng)
這篇文章主要為大家詳細介紹了python+mysql實現(xiàn)個人論文管理系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-10-10python 循環(huán)讀取txt文檔 并轉(zhuǎn)換成csv的方法
今天小編就為大家分享一篇python 循環(huán)讀取txt文檔 并轉(zhuǎn)換成csv的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-10-10