Python 中的 lambda 和「真正的」lambda 有什麼區別?
為什麼有人說 Python的 lambda 不是真正的 lambda(來自王垠博客)?或者 C# 對 FP 的支持比Python 好得多(來自劉汝佳《演算法競賽入門經典:訓練指南》)?
就我的認識來說,Python支持在任意位置定義函數(匿名函數),將函數作為一等參數,支持Currying,閉包,就應該是「真正的 lambda 」了。
Python的lambda里只能寫一行啦、不能有statement只能有expression啦,這些還是小問題,真正的問題是Python對Closure的實現根本是有缺陷的。閉包的實現都是錯誤的,哪來的真正的匿名函數?
比如在Python2里這樣的代碼是沒法運行的,def counter():
count = 0
def inner():
count += 1
return count
return inner
c = counter()
print c()
Python告訴你一個UnboundLocalError,count為什麼會unbound呢,因為closure沒有正確地實現。什麼是closure呢,closure是一個二元組:lambda(別管是有名字的還是沒名字的),和這個lambda定義時的environment。而這個environment包含了lambda中的自由變數(比如這裡的count),這樣才把這個lambda『封閉』起來了,所以叫閉包。
我所理解的『真正的』的lambda是說:完整地支持higher-order function,即函數可以作為函數的參數,也可以作為函數的返回值,那怕引入了mutation。為了達到這一點,語言的實現需要正確地實現closure和lexical scope。而mutation和lexical scope是兩個正交的概念,Python因為有mutation而沒有完整實現lexical scope進而沒有完整地支持first-order function,這就叫broken lambda。Python3里新加的nonlocal關鍵字就是為了解決closure的歷史問題。
然而同樣的代碼在Racket/Scala/OCaml里卻可以跑地歡快:(define (counter)
(define count 0)
(define (inner)
(begin (set! count (add1 count))
count))
inner)
(define c (counter))
(c) ;1
(c) ;2
(c) ;3
def counter(): () =&> Int = {
var count = 0
def inner() = {
count += 1
count
}
inner
}
val c = counter()
println(c())
println(c())
println(c())
let counter () =
let count = ref 0 in
let inner () =
count := !count + 1;
!count
in inner
;;
let c = counter();;
print_int(c());
print_int(c());
print_int(c());
真正的lambda就是正確而完整地實現了lexical scope和closure的lambda。這就是python的lambda和『真正的』的lambda的區別。
當然Python並不是函數式語言,Python也從來沒有自我標榜是函數式語言,當年lambda都是一個Lisp程序員給Python加的,而且據說當時Guido是強烈反對的……
BTW,lambda這個名字確實沒什麼神秘的:
===Update:經靈劍提醒,由於Racket和Python中對於list comprehension的實現不同,list comprehension的例子是不太恰當的。Racket中的list comprehension經過宏展開後是遞歸的函數調用的形式,而類似的python代碼可能是這樣的:map(lambda i: lambda n: i+n, range(10))[3](4)
這個時候Python的行為和Racket是一樣的。但對於list comprehension而言,Python並不是函數式語言(again),同Haskell、Scala、Racket這些的實現是不同的,在comprehension的過程中並沒有創建出各自包含i的閉包。
原:比如這個Python代碼:fs = [(lambda n: i + n) for i in range(10)]
fs[3](4)
fs[3](4)應該是幾呢?Python告訴你是13 = = 因為每一個lambda都share了相同的i。
同樣的代碼再看看Racket里呢:(define fs
(for/list ([i (range 10)])
(λ (n) (+ i n))))
((fourth fs) 4)
Racket里正確地告訴你結果是7。
真正的 lambda 就是沒有名字的函數,能做到任何普通函數能做到的事情,除了方便地遞歸調用自己。
Python 因為本身設計思想,僅僅將 lambda 定位成一個輔助用的短函數,為了避免使用中為一些臨時的小代碼專門還要寫 def 。
比如說對複雜的數據結構排序,要用函數選擇數據結構中排序的依據數據,那麼寫一個一次性的函數:def select(data):
return data.array[0]
sorted(data, key=select)
sorted(data, key=lambda x:x.array[0])
除類似此之外不提倡用 lambda,lambda 的局限主要就是為了防止濫用。所以 lambda 只能寫一行,不能賦值,內部也不能定義等等等。
這就是 Python 的方法論,雖然 Python 也有常用的函數式工具,但是使用的時候必須要謹慎,不要因為賣弄聰明降低可讀性和性能。
另外對於問題補充,Currying 是沒有支持的,而且這一堆都是函數式編程的特性,匿名函數和它們是平級的,是函數式編程特性的一部分,不存在從屬關係。
P.S : 我直接將問題中的 lambda 我直接理解成匿名函數了,但是 lambda 本身有別的含義,我大概沒理解錯。很煩「真正的」這個詞。
lambda 在程序語言中的表現,就是匿名函數。匿名函數,就是個沒有名字的函數。匿名函數比起有名字的函數,就是少了個名字,多不出什麼東西來。
函數式編程,真正重要的,是函數作為一等公民。同時,這也意味著要有高階函數,要在函數裡面也能定義函數,函函數數無窮盡也。Python 這點沒問題。
那些叫囂著已經有或者新增了 lambda 的語言,好些(不是所有)就是支付不起真正的一等公民函數,搞了個半殘的 lambda, 還要裝象,顯得很高級。
再說 python 的 lambda 和普通函數有什麼區別。最主要的,lambda 裡面只能有一個表達式作為返回值,而普通的函數可以有多行語句。看上去匿名函數功能弱的,但這根本不是什麼問題,因為我們需要的是一個「函數」,而不是「匿名函數」。(python裡面匿名函數不具有越超函數的能力,見上。)如果某個需要函數的地方,光用一個表達式不夠,那麼就地定義一個函數,作為扔進去就好了。Python 中 lambda 函數的限制,是故意這麼做的。原因很簡單,Python 把函數的可讀性看得重。如果一個沒名字的函數太長了,容易讓程序看起來混亂,不容易看懂。而給這個函數起個名字,也有助於增加函數的可讀性。
Zen of Python 其中的一條,就是 "flat is better than nested." (怕翻譯得太差,就不翻譯了)匿名函數中寫多行,增加了嵌套(nested),也是也可讀性相背的。這個問題,說到底,還是因為有些人對 lambda 這個詞抱著莫名的崇敬。大概因為 lambda 這個詞從 lambda calculas 這地方來的,而一旦沾上數學,而且是自己看不懂的數學。某些人立即就會覺得那個東西高端大氣上檔次了。另外一方便,某些人也沒見過一等公民函數長啥樣,忽然見了個 lambda, 便把一些好東西,都歸功於 「匿名函數」(lambda) 這個詞身上了,彷彿是「匿名」帶來的,說到底只是因為「函數」呀。關於辣個for循環的問題,我也來湊個熱鬧(defun test (i next p f result)____(if (p i) result (test (next i) p f (f i result)))(test 1
___(lambda (x)
______(eq x 1))____(lambda (x)________(+ x 1))____(lambda (i result)_________(append result (list (lambda (n)____________________________(+ n i)))))______nil)這一看就看出來10個叫做i的東西不是一個i(defmacro (x)___(let ((y (gensym))
_______(z (gensym)))__________`(lambda (,y)______________(if (eq ,y "get) _________________,x_________________(lambda ,(z)_____________________(setq ,x ,z))))))(defun * (p)____(funcall p "get))(defun *= (p content)______(funcall (funcall p "set) content))
(let* ((i 1) (ai ( i)))__(test ai _____(lambda (ax)_________(eq 10 (* ax)))_____(lambda (ax)_________(progn (*= (+ (* ax) 1))________________ax))_____(lambda (ax result)________(append result
________________(list (lambda (n)________________________(+ n (* ax))))))______nil))這個一眼就看出來所有lambda里都是i的引用(let* ((i 1)_____(ai ( i))) (test ai ___(lambda (ax)______(eq 10 (* ax)))___(lambda (ax)
______(progn (*= (+ (* ax) 1))_____________ax))___(lambda (ax result)______(let ((x (* ax)))________(append result _________________(list (lambda (n)_________________________(+ n (* ax)))))))____nil)))這個一眼就看出來所有lambda裡面都是i的引用傳給它時i的值所以看著你萌一會兒引用一會兒值噠本萌新表示實在是暈很慚愧,只做了一點微小的工作,謝謝大家補充一點 python是支持閉包的 只是被閉包的變數正常情況下不能修改
再回答一下自己的問題:即使Python可以在任意地方定義函數,它依然不能正確實現閉包。比如
funcs = []
def foo(m):
for i in xrange(m):
def bar(n):
return n + i
funcs.append(bar)
foo(10)
print funcs[3](5)
結果是14,因為 bar 存放的是i的引用,而非i的值。這樣的問題同樣存在於JavaScript中。
下面這段的輸出結果不符合有些人的預期,於是他們把Python批判一番。
funcs = [(lambda n: i * n) for i in range(5)]
print funcs[0](2)
print funcs[1](2)
print funcs[2](2)
print funcs[3](2)
print funcs[4](2)
主要問題還是在那個「for i in xxx」上,這個語義是說環境中有一個i,這個i的值在不斷變化,每次創建的閉包都引用這個i,所以這些閉包中i的值都該是一樣的,因為那是同一個i。
同樣的事情發生在Common Lisp中(總該承認Common Lisp是支持函數式的吧),寫了兩段作為對比。代碼後面是輸出。
其中第一段,創建時比照前面Python代碼的語義,在閉包創建的環境中有個n,n在不斷變化,引用n來創建閉包(lambda () n),意思是返回n。第二段,先創建一個列表,內容是數字0-4,再用這個列表中的每個值i,創建閉包(lambda () i),意思是返回i。(let ((func-lst (loop for n from 0 to 4
collect (lambda () n))))
(dolist (i func-lst)
(format t "~A: ~A~%" i (funcall i))))
#&
#&
#&
#&
#&
(let* ((lst (loop for n from 0 to 4 collect n))
(func-lst (mapcar (lambda (i)
(lambda () i))
lst)))
(dolist (i func-lst)
(format t "~A: ~A~%" i (funcall i))))
#&
#&
#&
#&
#&
聽說python的lambda表達式不能修改外部變數的同時還讓別的地方的代碼感受到,跟C++的lambda表達式沒有任何區別,跟C#和Java和其他正常的語言完全不同
那些說C#裡面是對的Python裡面的是錯的你們真的夠了。Lambda表達式是函數式編程裡面的概念,函數式編程根本就沒有變數,所有的符號從定義開始值就不會改變,讓Lambda能引用變數相當於一種擴展。這種引用有兩種實現,一種在定義時綁定,一種在執行時綁定,前者以C#等為代表,後者以Python、Javascript為代表。我要說的是,明顯Python這種實現才是正確的、符合函數式編程特性的,因為只有這樣才能正確在Lambda表達式中調用自己實現遞歸:
lambda_list = lambda n: lambda_list(n-1) + (lambda x: x * (n-1),) if n else ()
fs = lambda_list(10)
fs[3](5) # 15
你如果是定義時綁定,這個表達式定義的時候lambda_list是綁定不到東西的,會報錯,這就是為什麼C#當中只允許在函數中定義匿名函數而不能定義命名函數,因為定義了就會暴露出無法遞歸的缺陷。這明明就是設計錯了,或者偷懶用了簡單的實現(定義時綁定遠遠比運行時綁定簡單)
當然其實從數學上來說,遞歸可以用運算元間接實現,但沒有人會願意在實際編程的時候每次都寫一遍吧。
你們在循環中創建lambda表達式,本來就違背了函數式編程最重要的原則即所有符號都綁定到確定的值,然後你還說這個結果不對,然後你還說是Python設計錯了,你們真的夠了和實際的lambda並沒有什麼實質上的區別
Y = lambda f: (lambda x: f(lambda y: (x)(x)(y)))(lambda x: f(lambda y: (x)(x)(y)))
g = lambda fib: lambda n: 0 if n == 0 else (n + (fib(n - 1)))
f = Y(g)
f(5) # =&> 15
fs = [lambda n: i + n for i in range(10)]
print fs[0](4)
i = 100
print fs[0](4)
13
104
所以 i 是真正要使用時才取值的。這裡的 i 和 fs 處於同一個命名空間。
比如下面這句fs = [lambda n: i + "a" for i in range(10)]
並不會報錯 (運行時才會報錯)。
下面這個運行時也會報錯c = 1
def f():
c += 1
return c
f()
當函數內部出現
c = ...
這樣的語句時,c 被認為是局部變數,而不會去外部命名空間里找,於是出錯。但如果 c 出現在右邊則不會出錯。所以
def counter(count = 0):
def inner():
c = count
c += 1
return c
return inner
c = counter()
print c()
輸出是 1,是正確的。
lambda就是個匿名函數。。和閉包的變數捕捉方式沒關係啊
y = 10
fn = lambda x : y+=x
# python will raise error .
推薦閱讀: