除了def語句之外,Python還提供了一種生成函數對象的表達式形式。由於它與LISP語言中的一個工具很相似,所以稱為lambda。就像def一樣,這個表達式創建了一個之后能夠調用的函數,但是它返回了一個函數而不是將這個函數賦值給一個變量名。這也就是lambda有時叫做匿名函數的原因。實際上,他們常常以一種行內進行函數定義的形式使用,或者用作推遲執行一些代碼。
lambda表達式
lambda的一般形式是關鍵字lambda,之后是一個或多個參數(與一個def頭部內用括號括起來的參數列表及其相似),緊跟的是一個冒號,之后是一個表達式:
lambda argument1,argument2,... argumentN:expression using argument
由lambda表達式所返回的函數對象與由def創建並復制后的函數對象工作起來是完全一樣的,但是lambda由一些不同之處讓其在扮演特定的角色時很有用。
-
lambda是一個表達式,而不是一個語句。因為這一點,lambda能夠出現在Python語法不允許def出現的地方——例如,在一個列表常量中或者函數調用的參數中。此外,作為一個表達式,lambda返回了一個值(一個新的函數),可以選擇性的賦值給一個變量名。相反,def語句總是得在頭部將一個新的函數賦值給一個變量名,而不是講這個函數作為結果返回。
-
lambda的主體是一個單個的表達式,而不是一個代碼塊。這個lambda的主體簡單得就好像放在def主體的return語句中的代碼一樣。簡單地將結果寫成一個順暢的表達式,而不是明確的返回。因為它僅限於表達式,lambda通常要比def功能要小:你僅能夠在lambda主體中封裝有限的邏輯進去,連if這樣的語句都不能夠使用。 這是有意設計的——它限制了程序的嵌套:lambda是一個為編寫簡單的函數而設計的,而def用來處理更大的任務。
除了這些差別,def 和 lambda都能夠做同樣種類的工作。例如,我們見到了如何使用def語句創建函數。
# ###################### 普通函數 ###################### # 定義函數(普通方式) def func(arg): return arg + 1 # 執行函數 result = func(123)
但是,能夠使用lambda表達式達到相同的效果,通過明確地將結果賦值給一個變量名,之后就能夠通過這個變量名調用這個函數。
# ###################### lambda ###################### # 定義函數(lambda表達式) my_lambda = lambda arg : arg + 1 # 執行函數 result = my_lambda(123)
這里的f被賦值給一個lambda表達式創建的函數對象。 這也就是def所完成的任務,只不過def的賦值是自動進行的。
默認參數也能夠在lambda參數中使用,就像在def中使用一樣。
>>> x = (lambda a="fee", b="fie", c="foe": a + b + c) >>> x("wee") 'weefiefoe'
在lambda主體中的代碼想在def內的代碼一樣都遵循相同的作用於查找法則。lambda表達式引入的一個本地作用域更像一個嵌套的def語句,將會自動從上層函數中、模塊中 以及內置作用域中(通過LEGB法則)查找變量名。
>>> def knights(): ... title = "Sir" ... action = (lambda x: title + ' ' + x) ... return action ... >>> act = knight() >>> act('robin') 'Sir robin'
在Python 2中,變量名title的值通常會修改為通過默認參數的值傳入。
為什么使用lambda
通常來說,lambda起到了一種函數速寫的作用,允許在使用的代碼內嵌入一個函數的定義。他們完全是可選的(你總是能夠使用def來替代它們),但是你僅需要嵌入小段可執行代碼的情況下它們會帶來一個更簡潔的代碼結構。
例如,我們在稍后會看到回調處理器,它常常在一個注冊調用(registration call)的參數列表中編寫成單行的lambda表達式,而不是使用在文件其他地方的一個def來定義,之后引用那個變量名。
lambda通常用來編寫跳轉表(jump table),也就是行為的列表或字典,能夠按照需要執行相應的動作。如下段代碼所示。
L = [lambda x: x**2, lambda x: x**3, lambda x: x**4] for f in L: print(f(2)) # prints 4, 8, 16 print(L[0](3)) # prints 9
當需要把小段的可執行代碼編寫進def語句從語法上不能編寫進的地方時,lambda表達式作為def的一種速寫來說是最為有用的。 例如,這種代碼片段,可以通過在列表常量中嵌入lambda表達式創建一個含有三個函數的列表。一個def是不會再列表常量中工作的,因為它是一個語句,而不是一個表達式。 對等的def代碼可能需要在想要使用的環境之外有臨時性的函數名稱和函數定義。
def f1(x): return x**2 def f2(x): return x**3 def f3(x): return x**4 L = [f1, f2, f3] for f in L: print(f(2)) # prints 4, 8, 16 print(L[0](3)) # prints 9
實際上,我們可以使用Python中的字典或者其他的數據結構來構建更多種類的行為表,從而做同樣的事情。 下面是以交互提示模式給出的另一個例子:
>>> key = 'got' >>> {'already': (lambda: 2 + 2), ... 'got': (lambda: 2 * 4), ... 'one': (lambda: 2 ** 6)}[key]() 8
這里,當Python常見這個字典的時候,每個嵌套的lambda都生成並留下了一個在之后能夠調用的函數。通過鍵索引來取回其中一個函數,而括號使去除的函數被調用。 與在之前向你展示的if語句的擴展用法相比,這樣編寫代碼可以使字典成為更加通用的多路分支工具。
如果不是用lambda做這種工作,需要使用三個文件中其他地方出現過的def語句來替代,也就是在這些函數將會使用的那個字典外的某處需要定義這些函數。
>>> def f1(): return 2 + 2 ... >>> def f2(): return 2 * 4 ... >>> def f3(): return 2 ** 6 ... >>> key = 'one' >>> {'already': f1, 'got': f2, 'one': f3}[key]() 64
同樣,會實現相同的功能,但是def也許會出現在文件中的任意位置,即使它們之后很少的代碼。類似剛才lambda的代碼,提供了一種特別有用的可以在單個情況出現的函數:如果這里的三個函數不會在其他的地方使用到,那么將它們定義作為lambda嵌入在字典中就很合理了。不僅如此,def格式要求為這些小函數創建變量名,這些變量名也許會與這個文件中的其他變量名發生沖突(也可能不會,但總是可能的)。
如何(不要)讓Python代碼變得晦澀難懂
由於lambda的主體必須是個表達式(而不是一些語句),由此可見僅能將有限的邏輯封裝到一個lambda中。如果你知道在做什么,那么你就能在Python中作為基於表達式等效的寫法編寫足夠多的語句。
例如,如果你希望在lambda函數中進行print,直接編寫sys.stdout.write(str(x) + "\n") 這個表達式,而不是使用print(x)這樣的語句。 類似地,要在一個lambda中潛逃邏輯,可以使用if/else三元表達式,或者對等的但需要些技巧的and/or組合。正如我們前面所了解到的,如下語句:
if a: b else: c
能夠由以下的概括等效的表達式來模擬:
b if a else c ((a and b) or c)
因為這樣類似的表達式能夠放在lambda中,所以它們能夠在lambda函數中來實現選擇邏輯。
>>> lower = (lambda x, y: x if x < y else y) >>> lower('bb', 'aa') 'aa' >>> lower('aa', 'bb') 'aa'
此外,如果需要在lambda函數中執行循環,能夠嵌入map調用或列表解析表達式這樣的工具來實現。
>>> import sys >>> showall = lambda x: list(map(sys.stdout.write, x)) >>> t = showall(['spam\n', 'toast\n', 'eggs\n']) spam toast eggs >>> showall = lambda x: [sys.stdout.write(line) for line in x] # 列表解析 >>> t = showall(['spam\n', 'toast\n', 'eggs\n']) spam toast eggs
這些技巧必須在萬不得已的情況下才使用。一不小心,它們就會導致不可讀(也成為晦澀難懂)的Python代碼。 一般來說,簡潔優於復雜,明確優於晦澀,而且一個完整的語句要比神秘的表達式要好。 這就是為什么lambda僅限於表達式。如果你有更負責的代碼要編寫,可使用def, lambda針對較小的一段內聯代碼。 從另一個方面來說,你也會發現濕度的使用這些技術是很有用處的。
嵌套lambda和作用域
lambda是嵌套函數作用域查找(LEGB原則中的E)的最大受益者。 例如,在下面的例子中,lambda出現在def中(很典型的情況),並且在商城函數調用的時候,嵌套的lambda能夠獲取到上層函數作用域中的變量名x的值。
>>> def action(x): return (lambda y: x + y) >>> act = action(99) >>> act <function action.<locals>.<lambda> at 0x0000014EF59F4C80> >>> act(2) 101
在之前講關於嵌套函數作用域的討論沒有標明的就是lambda也能夠獲取任意上層lambda中的變量名。 這種情況有些隱晦,但是想象一下,如果我們上一個例子中高端def換成一個lambda。
>>> action = (lambda x:(lambda y: x + y)) >>> act = action(99) >>> act(3) 102 >>> ((lambda x: (lambda y: x + y))(99))(4) 103
這里嵌套的lambda結構讓函數在調用時創建了一個函數。無論以上那種情況,嵌套的lambda代碼都能夠獲取上層lambda函數中的變量x。這可以工作,但是這種代碼讓人相當費解。處於可讀性的要求,通常來說,最好避免使用嵌套的lambda。