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