據(jù)操作之lambda表達(dá)式詳情)
前言lambda是 Python 創(chuàng)建匿名函數(shù)anonymous function的語(yǔ)法。它解決的問題很具體當(dāng)你需要一個(gè)「只用一次的小函數(shù)」當(dāng)參數(shù)傳進(jìn)去時(shí)比如sorted的排序鍵、map/filter的處理邏輯單獨(dú)寫一個(gè)def再起個(gè)名字顯得多余。lambda讓你把函數(shù)就地寫出來(lái)。標(biāo)題里說(shuō)這是「數(shù)據(jù)操作」這個(gè)說(shuō)法只對(duì)了一半。lambda本身不做任何數(shù)據(jù)操作它只是創(chuàng)建函數(shù)對(duì)象的一種寫法真正在處理數(shù)據(jù)的是接收它的那些函數(shù)sorted、max、map等。把lambda當(dāng)成「數(shù)據(jù)處理工具」容易導(dǎo)致濫用。另外兩個(gè)常見誤解要提前說(shuō)清楚以為lambda是「函數(shù)的一行簡(jiǎn)寫」里面可以寫任意邏輯。實(shí)際上lambda的函數(shù)體只能是單個(gè)表達(dá)式不能包含語(yǔ)句——沒有賦值語(yǔ)句、沒有for/while、沒有try。以為lambda比def快。同一個(gè)函數(shù)體無(wú)論用lambda還是def運(yùn)行時(shí)都是普通的函數(shù)對(duì)象調(diào)用開銷沒有本質(zhì)區(qū)別。寫lambda應(yīng)該是為了就地、簡(jiǎn)潔而不是為了性能。本文先講清語(yǔ)法邊界再講它真正合適的場(chǎng)景最后重點(diǎn)講那個(gè)幾乎人人踩過(guò)的坑閉包在循環(huán)里的延遲綁定。一、lambda 的語(yǔ)法與邊界語(yǔ)法形式是lambda 參數(shù)列表: 表達(dá)式它等價(jià)于一個(gè)只含return 表達(dá)式的def函數(shù)。下面兩種寫法在運(yùn)行時(shí)行為一致# 適用于 Python 3.8def add_def(x, y):return x yadd_lambda lambda x, y: x yprint(add_def(2, 3), add_lambda(2, 3)) # 5 5參數(shù)部分和普通函數(shù)一樣可以有默認(rèn)值、可以收集可變參數(shù)3.8 起也能用僅位置參數(shù)/# 適用于 Python 3.8f1 lambda a, b10: a bf2 lambda *args, **kwargs: (len(args), len(kwargs))f3 lambda a, /, b: a * b # 僅位置參數(shù)3.8print(f1(1)) # 11print(f2(1, 2, x3)) # (2, 1)print(f3(2, 3)) # 6邊界也很明確函數(shù)體是表達(dá)式不是語(yǔ)句塊。lambda x: y x是語(yǔ)法錯(cuò)誤是語(yǔ)句里的賦值。不能寫注解、不能寫文檔字符串。lambda函數(shù)對(duì)象的__name__是字符串lambdarepr里也只會(huì)顯示內(nèi)存地址。無(wú)法在自己體內(nèi)遞歸調(diào)用自己因?yàn)樗鼪]有可引用的名字。需要多行邏輯、需要中間變量、需要異常處理時(shí)就應(yīng)該老老實(shí)實(shí)寫def。二、lambda 真正合適的幾個(gè)場(chǎng)景場(chǎng)景一作為排序/取極值的 key。這是lambda最經(jīng)典、最不容易被濫用的用法。# 適用于 Python 3.8rows [(bob, 90), (ann, 95), (cid, 88)]rows.sort(keylambda r: r[1], reverseTrue)print(rows) # [(ann, 95), (bob, 90), (cid, 88)]print(max(rows, keylambda r: r[1])) # (ann, 95)注意list.sort()是原地排序、返回None別寫成rows rows.sort(...)。場(chǎng)景二作為map/filter的處理函數(shù)。但要記得Python 3 里它們返回的是迭代器不是列表。# 適用于 Python 3.8nums [1, 2, 3, 4]squares map(lambda n: n * n, nums)print(list(squares)) # [1, 4, 9, 16]print(list(filter(lambda n: n % 2 0, nums))) # [2, 4]場(chǎng)景三作為回調(diào)callback。例如事件觸發(fā)、按鈕點(diǎn)擊的命令函數(shù)往往只需要一句調(diào)用用lambda就地寫很自然。# 適用于 Python 3.8handlers {add: lambda a, b: a b,sub: lambda a, b: a - b,}print(handlers[sub](10, 3)) # 7用一個(gè)字典把名字映射到函數(shù)比寫一長(zhǎng)串if/elif更清晰也更容易擴(kuò)展。三、閉包與循環(huán)里的延遲綁定重點(diǎn)這是lambda最著名、癥狀也最詭異的坑。lambda函數(shù)體里引用的變量是在函數(shù)被調(diào)用時(shí)才去外層作用域查找的而不是在定義時(shí)把值抄進(jìn)去。# 適用于 Python 3.8funcs [lambda: i for i in range(3)]print([f() for f in funcs]) # [2, 2, 2]不是 [0, 1, 2]三個(gè)lambda共享同一個(gè)外層變量i。循環(huán)結(jié)束時(shí)i已經(jīng)是2所以調(diào)用任何一個(gè)都返回2。這叫延遲綁定late binding是閉包的定義性行為不是 bug。修法一用默認(rèn)參數(shù)在定義時(shí)求值。默認(rèn)參數(shù)的值是在def/lambda被創(chuàng)建的那一刻算好的# 適用于 Python 3.8funcs [lambda ii: i for i in range(3)]print([f() for f in funcs]) # [0, 1, 2]修法二寫一個(gè)工廠函數(shù)把變量固定成參數(shù)# 適用于 Python 3.8def make_func(i):return lambda: ifuncs [make_func(i) for i in range(3)]print([f() for f in funcs]) # [0, 1, 2]工廠函數(shù)更啰嗦但讀起來(lái)意圖更明顯變量一多時(shí)不容易寫錯(cuò)。四、一個(gè)可運(yùn)行的完整示例把上面的點(diǎn)串起來(lái)按多個(gè)字段排序并用字典做函數(shù)分發(fā)。# 適用于 Python 3.8def sort_students(students):# 先按分?jǐn)?shù)降序分?jǐn)?shù)相同時(shí)按姓名升序return sorted(students, keylambda s: (-s[score], s[name]))def summarize(students):total sum(s[score] for s in students)avg total / len(students) if students else 0.0names , .join(s[name] for s in students)return f平均分 {avg:.1f}共 {len(students)} 人{(lán)names}if __name__ __main__:data [{name: bob, score: 90},{name: ann, score: 95},{name: cid, score: 90},]ordered sort_students(data)print([s[name] for s in ordered]) # [ann, bob, cid]print(summarize(ordered))用-s[score]而不是reverseTrue是為了讓「分?jǐn)?shù)降序、姓名升序」這種混合方向能一次表達(dá)出來(lái)。常見坑點(diǎn)循環(huán)里延遲綁定?funcs [lambda: i for i in range(3)]調(diào)用后全是2?funcs [lambda ii: i for i in range(3)]得到[0, 1, 2]在 lambda 體里寫賦值語(yǔ)句?f lambda x: y x 1直接SyntaxError? 需要中間變量就改用def或把計(jì)算放到調(diào)用前完成用 lambda 后__name__丟失? 日志里打印lambda出問題不知道是哪個(gè)函數(shù) ? 需要可讀的函數(shù)名和調(diào)試信息時(shí)老老實(shí)實(shí)寫def把list.sort的結(jié)果賦回去?rows rows.sort(keylambda r: r[1])rows變成None?rows.sort(keylambda r: r[1])原地排序不要再賦值以為 Python 3 的map/filter返回列表?n len(map(lambda x: x, data))TypeError? 需要長(zhǎng)度或重復(fù)遍歷時(shí)先list(...)包一層它們只返回迭代器想用 lambda 做多進(jìn)程任務(wù)函數(shù)?Pool.map(lambda x: x * 2, data)會(huì)因無(wú)法 pickle 而失敗 ? 用模塊頂層定義的有名函數(shù)lambda不可序列化用 lambda 代替functools.reduce做求和?reduce(lambda a, b: a b, nums)還得多 import ? 求和直接用sum(nums)更短也更容易讀懂嵌套 lambda 導(dǎo)致可讀性崩壞?sorted(d, keylambda r: (lambda x: x[1])(r))? 拆成獨(dú)立的def或者用operator.itemgetter(1)總結(jié)維度lambdadef函數(shù)體只能是單個(gè)表達(dá)式任意語(yǔ)句塊名字固定為lambda可讀的__name__注解 / 文檔不支持支持自遞歸不行行可 pickle不行模塊頂層函數(shù)可以適合場(chǎng)景就地傳參的短回調(diào)、排序鍵任何有名字、有邏輯的函數(shù)lambda的定位是「就地寫的小函數(shù)」用對(duì)了很省事用錯(cuò)了兩類問題最要命一類是表達(dá)能力不夠只能硬湊另一類是循環(huán)里的延遲綁定。后者只要記住「默認(rèn)參數(shù)在定義時(shí)求值」這條規(guī)律就能穩(wěn)定避開。