標籤:

Python 所謂的「閉包」是不是本著故意把人搞暈的態度發明出來的?

又或者稱「裝B」的態度發明出來的? 請看:Python深入04 閉包, 裡面有「閉包」的一個實際例子:

def line_conf(a, b):
def line(x):
return a*x + b
return line

line1 = line_conf(1, 1)
line2 = line_conf(4, 5)
print(line1(5), line2(5))

要我寫我會這樣:

def line_conf(a, b, x):
return a*x + b
line_conf(1, 1, 5)
line_conf(4, 5, 5)

我的程序多麼簡單好懂實用啊,一般人都會像我這麼寫吧? 再看看上面那一坨代碼。

能不能給出一個Python閉包真正有用的例子?


閉包不是 Python 的概念,是所有 function-as-first-class 的語言都會涉及的概念。

你所看到「實際例子」根本不實際。就像你說的,一般人都不會那麼蛋疼。國內許多教材都只講「怎麼做」,而不講「為什麼這麼做」,所以你不能理解是很正常的。

來一個講解閉包時會用到的真正有用的例子:

def new_counter():
i = 0
def count():
nonlocal i
i += 1
return i
return count

然後你可以

a_count = new_counter()
b_count = new_counter()
a_count(); a_count()
b_count()

它們是獨立的兩個計數器。

當然這個例子也只是教學用的,遠不是真實世界裡的需求。至於真實世界裡的閉包,很抱歉,當我遇到的時候,或者寫出一個的時候,我並不會意識到「哇,這是一個閉包耶」,所以一時舉不出例子來。

在基於回調的事件系統中,也許 partial 是可以用閉包實現的重要應用。不過 Python 標準庫的 partial 是用類實現的。反正閉包只是一個特性,有沒有它並不影響語言的圖靈完備性,只是寫起來讀起來方不方便的事情。

最後,我奉勸你看 cnblogs 啊 iteye 啊 oschina 啊上的文章時小心一點,誤人子弟的內容太多。閉包和並行計算沒那麼大的關係,Python 的閉包和並行計算完全沒關係(因為單進程並行不了,有 GIL;多進程數據隔離閉包沒特別的作用)。閉合與函數式編程也沒有特別的關係。

最後,我很贊同硝酸銨的回答,這裡是鏈接 Python所謂的「閉包」是不是本著故意把人搞暈的態度發明出來的? - 硝酸銨的回答,知乎的 @ 體驗差到爆表就算了,竟然只能 @ 到硝酸銀和硝酸銅。知乎啊知乎,硝酸銨雖然易爆還有毒,但也沒天津那堆化工品危險,你不用害怕的啦……


簡短答案:前者更具有可讀性和可移植性。

較完整答案:閉包(closure)和類(class)有相通之處,帶有面向對象的封裝思維。而面向對象編程正是為了更佳的可讀性和更關鍵的可移植性;不過這個例子沒太體現出面向對象的額外優勢。

升級答案:題主問出這個問題,很可能是現在流行的編程教材知其然不知其所然的風格帶來的惡果。如果未來帶著這個想法進入IT行業,會非常不適應公司的代碼規範等基礎要求;即便不入行只是自己寫寫程序用,也會和很多優秀、可復用等的理念失之交臂。

先就事論事討論可讀性和可移植性:

例子中的程序做的事是根據兩條不同的直線方程和其上點的X值計算Y值。代碼風格一做的是先定義整個直線方程的類型,再據此提供斜率、截距兩個參數定義直線的函數,再調用函數根據X計算Y坐標。風格二是定義一個包含斜率、截距、X坐標三參數的直線方程函數,直接調用計算坐標。

題主覺得代碼風格二更簡單是因為你兩條直線都各只計算一個點。如果能確定這段程序的用途真的就只計算這麼兩個點,那確實後者簡潔。

但是……

  • 假如直線一要求計算10個點,直線二要求計算50個,你用風格二寫寫看?

  • 然後假如突然老闆告訴你不好意思直線二的參數有變,你再改改風格二的代碼看?

  • 再假如過一陣老闆說另外還有直線三四的參數我們終於確定了,兩天後用上。你覺得是現在就在風格一的代碼里添加兩條新直線的定義函數,兩天後調用新函數好;還是等兩天再翻郵件找參數(或者更糟,拚命回憶老闆前天說過的四個數字),然後追加風格二的代碼好?

  • 就算以上都沒發生,還有這種可能情況:仍然只算兩個點,只是隔了那麼百來行代碼,第二次調用的時候你還記得大明湖畔的三參數順序么?

其實光計算原題中兩個點的坐標,我還有風格三的代碼呢:

y1 = 1*5 + 1
# y1 = 1*1 + 5 ### 我相信這句和上句總有一個是大明湖畔的嗯嗯……
y2 = 4*5 + 5

看,不使用函數可以比風格二還少兩行,是不是最簡單好懂實用?由此推論,Python(以及所有編程語言)所謂的「函數」是不是本著故意把人搞暈的態度發明出來的,又或者稱「裝B」的態度發明出來的?

聲明一下,三種代碼並非有絕對的優劣之分,或者存在什麼「鄙視鏈」一類的東西。上面最後一句是對原問題流露出的某種「反向鄙視」趨向(「我用不上的都沒用,我看不懂的都是裝」)的回應。就我個人而言,三種都有可能使用:大部分情況寫代碼是風格一;很短的程序而且確定此部分不會增、改、復用很可能會用風格二;一時找不到計算器只是提示符下算個坐標當然選風格三。

再略微提一下面向對象:

對於原題這個求坐標的需求,不採用閉包,通過函數間接調用也能實現類似效果,同時保證可讀性和可移植性。為何要用閉包涉及更多面向對象和封裝的理念了。其它答案中舉出了不少例子,例如 @依雲 提到的兩個獨立計數器的例子就是非常好的典型,光用函數間接調用非常難實現。

不過,我覺得看題主所比較的兩種代碼風格,暫時還沒到追究這一步的狀態……

然後說形而上的東西,猜測題主為什麼會有這種想法:

現在絕大部分編程教材的風格都是:只告訴你這功能能做什麼事,然後跟個超級簡單的例子;幾乎不會提什麼事該用什麼功能實現、為什麼最好用這功能實現。從例子中常常一點也看不出用這功能能帶來什麼好處,反倒像是刻意不採用更簡單方法解決就為了呈現效果人為硬上這個功能。(最集中的地方是最後的內置函數甚至標準庫函數列表:羅列這語言有什麼功能,根本不教你什麼時候用和使用時要注意什麼——通常這麼個基本不會用上的附錄能增加1/20以上的厚度和書價。)

我覺得這種流行的教材風格,例如題主讀到的教程中這個不解釋用閉包有什麼好處的糟糕例子,是導致類似「搞這麼複雜有必要麼」的想法的重大原因之一。

我認為一本優秀的教材除了講基礎語法,應該盡量教你代碼為什麼這麼寫好、那麼寫不好、那麼那麼寫現在能用但多半以後會出事。

接著應該包含一定程度更深入的內容:編程語言發明者為什麼要設計這個功能,為什麼不用「簡單」方法實現,這些實現方法有什麼缺陷和隱患(特別是C這種比較基層的破壞力無止境的語言)。

這樣有朝一日進了公司才能理解,為什麼會有那麼多看起來很沒必要的代碼風格要求,為什麼「你管我怎麼寫能運行出正確結果不就行了」的想法會混不下去。(舉個栗子:有的公司規定判斷單變數是否為真只許用 if (true == x) 這種「廢到不能再廢的廢話」,不許用 if (x),也不許用 if (x == true) ——可以思考下為什麼。)或者就算是自己寫程序,半年到幾年後翻出當初自己的代碼重新修改用於新任務的時候,煩躁程度也可能輕一點。

判斷一本書在這點上用不用心的快速方法是看它函數提到遞歸的那段,會不會願意花點精力跟你講講遞歸和循環的相互可替代性,如果有的話我一般認為說明至少這章的作者很厚道(不一定能保證全書如此,很多教程標出來的作者只是編著者);如果還能藉機認真提下棧的理念,那就是更良心的作品了。

最後是沒收錢的廣告:

我在碼農學徒階段接觸過兩本不走尋常路的書,共同特點是充分強調編程理念,時刻在解答「為什麼」,而不僅是「有什麼」。它們對我個人影響非常大,很想推薦給大家(無利益關聯,真心想感謝作者……另外我承認我是先看的電子版然後主動追補購買交費的):

  1. 《一站式學習C編程(升級版)》,宋勁杉 著,電子工業出版社,ISBN 9787121129827。作者同時將其舊版本大部分章節在GFDL協議下公開:http://songjinshan.com/akabook/zh/

  2. 《Perl最佳實踐》,Damian Conway 著,O"Reilly Taiwan公司 編譯,東南大學出版社,ISBN 9787564110093

兩本書中的編程理念都是跨語言的。前者同時兼C教科書,可以從零基礎開始;但對很熟悉C的同樣強烈推薦,它絕無僅有地將編程的最基礎理念和後台運作模式過了一遍。後者是各種實例的集合,更像本代碼風格規範指南,但會跟你解釋為什麼這樣的風格是好的;其中涉及Perl基本語法概念的部分只有很簡明的總結提示,所以一半例子不需要學過Perl,另一半最好預先有些Perl基礎,但若比較熟悉Perl的話則能從書中領悟到更多及推廣到更多其它語言。

另外,兩本書的作者都很幽默或者冷幽默。


O(∩_∩)O哈哈哈~,題注太可愛了2333

閉包有用啊,用處大大的有。他的好處在於可以提供一個包含一個你函數執行的上下文環境。就拿你題目中的例子來說吧,乍一看這麼寫好像很奇怪的樣子,主要是因為這個例子太簡單了,簡單到感覺不用閉包似乎也能夠很好地解決。但是我們設定個前提條件,讓他稍稍複雜些。

假設你要寫一個函數的繪圖模塊,你並不關心要畫的東西究竟是什麼,你只負責實現一個功能就是給出 x 坐標的值你能夠返回給我一個 y 坐標。的因此你告訴你所有的開發小夥伴,調用我這個繪圖的功能大家必須要符合一個介面,就是 foo(x) -&> y。

你為什麼這麼要求呢?因為你們的程序跑在一種異常複雜的硬體上,全世界就你一個人會這個玩意,實現這麼一個功能已經很累了。而且同事們的繪圖需求太多了,有人要畫橢圓,有人要畫拋物線,有人要畫伽利略螺旋等等,你不可能給每個人都寫一個單獨的介面提供這個功能。況且,有的東西你也不知道怎麼畫(譬如什麼伽利略螺旋)。

因為介面一樣,所以有很多好處,比如你的程序可能長這個樣子:

def draw(func_list):
for func in func_list:
for x in domain:
draw(x, func(x)) #異常複雜 ( ⊙ o ⊙ )

於是乎大家就風風火火的開幹了。

這個時候你們公司有個小明。小明是個新員工,因此他被分配到實現一些簡單地功能,比如調用你的介面去畫直線。但是這個時候有一個很操蛋的需求,就是這個直線的方程是根據用戶的一系列複雜的生理特徵動態生成的,比如斜率是24小時平均心跳速度,截距是上一個小時消耗的卡路里(別問我這個直線有什麼用,很複雜的!)。OK,因為是動態生成的,所以不能寫死啊,不能寫成醬紫:

def extreme_complex_line(x):
return 12*x+8

但是介面又是寫死的,所以你不能寫成下邊這樣:

def extreme_complex_line(heart_beats, ka_lu_li, x):
return heart_beats*x+ka_lu_li

況且你們公司有2000多名用戶,所以必然要有一個動態的方法去生成這些包含上下文信息的函數,怎麼做呢?聰明的你已經想到了:閉包!

def line_conf(heart_beats, ka_lu_li):
def _line(x):
return heart_beats*x+ka_lu_li
return _line

lines = [line_conf(a, b) for a,b in two_thousand_users_data]

draw(lines)

就是醬紫~??


朋友,你用過裝飾器沒?


你的程序不怎麼好懂吧。誰知道line_conf(4, 5, 5)是什麼鬼?

要是我我會這麼寫。

def line(func,x):
return func(x)

def straight_line_base(a,b):
def func(x):
return a*x+b
return func

line_base1 = straight_line_base(4,5)
print(line(line_base1,5))

或者我會用類:

class CurveBase:
def __init__(self, func=None):
if func:
self.base_function = func

def straight_line(self, gradient, y_intercept):
def func(*args):
return gradient*args[0]+y_intercept
self.base_function = func

def execute(self, *args):
if self.base_function:
try:
return self.base_function(*args)
except:
raise Exception("exception occurs when execute the curve function.")
else:
raise Exception("error:base function not initialized!")

line_base1 = CurveBase()
line_base1.straight_line(4, 5)
print(line_base1.execute(5))

其實你看我也很自然的用了閉包。

為什麼?因為我懶啊。

第一段代碼裡面,下次我只要重寫一個square_line_base就可以支持2次曲線方程了。

甚至我定義個ugly_line_base就可以生成某個很醜陋的歪七扭八的曲線方程了。

第二段代碼就更靈活了,你甚至可以支持自定義任意參數數量的方程。不管是直線也好,曲線也好,甚至是平面,都沒問題,只要定義一個正確的方程。然而這些不管你怎麼自定義,出口都只有一個:execute();

但是按照你那種寫法擴展起來就非常頭痛。

但是使用閉包並不是必須的。以第二段代碼為例,你可以繼承CurveBase來生成自己的LineBase,然後在init裡面定義斜率和截距:

class LineBase:
def __init__(self, gradient, y_intercept):
self.grad = gradient
self.y_intcpt = y_intercept

def exectue(self,x):
return self.grad*x + self.y_intcpt

完全沒問題。

或者你也可以用partial function或者用decorator來代替閉包實現動態生成曲線方程,完全沒有問題。

其實我說這麼多,都是廢話。

只是想告訴你:閉包不是必須的。

沒了閉包,python的功能一點不會被影響(如果你不把partial和decorator算成閉包的話)。

有了閉包,只是提供給你一種額外的解決方案。

編程語言要不要加入某個特效本來就是各方角力妥協的結果。所以你會看到很多看起來蛋疼,或者你覺得沒用的特性。但是這些特性大多數並不真的沒用——只是你習慣於使用其他特性來解決問題而已


簡單來說,沒有一個語言是故意為了把你搞暈而提出一個新特性的。

題目中這個例子:

1. 比你的代碼更實用:這個line_conf,本意是為了得到一條直線,之後可以直接通過x求出來ax+b,不用每次都配置a和b的值。你覺得是:

y1 = line_conf(1,1,1)
y2 = line_conf(1,1,2)

清晰簡單,還是

line = line_conf(1,1)
y1 = line(1)
y2 = line(2)

清晰簡單?

2. 也體現了閉包的作用:試想,返回的都是line這個函數,為什麼表現不一樣?這難道沒有體現閉包的所謂「局部作用域」的特點嗎?


這樣就暈了,如果你看到JS的作用域鏈不是要直接把電腦砸掉。

標準庫 functools 有個叫 partial 的函數,

def partial(func, *args, **keywords):
def newfunc(*fargs, **fkeywords):
newkeywords = keywords.copy()
newkeywords.update(fkeywords)
return func(*(args + fargs), **newkeywords)
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc

base2 = partial(int, base=2)
print base2("101010")
# 42


關於閉包我有這樣一個trick,實現一些hook的時候為了保證主流程中的對象不被不信任的外部callback修改,我會用閉包做這樣的一個proxy:

def proxy(obj):
class ProxyObject(object):
def __getattribute__(self, name):
return getattr(obj, name)

return ProxyObject()

我想不到有其他比用閉包更優雅的實現方式了


這兩個寫法都沒有問題。只是有一些細微的差別,並且命名都很令人誤會(畫線函數不應該是給一個x輸出一個y,而應該是給出一組x輸出一組y)。

你的寫法簡單粗暴。

而原來的實例也挺好,有點元編程的味道。下面幫助你理解一下。

用y=k*x+b來表示一條直線對吧?那麼,對於一組給定的k,b,可以表示一條唯一的直線對吧?

下面令k = 1 , b = 2

line_conf(1, 2)就生成了這麼一個畫線的函數。

line1 = func(x) { return 1 * x + 2; }


我寫過一個東西,可以把程序中的某一個函數發送到另一台機器算。比如這個程序:

def foo(x):
return sum(range(x))

print foo(1000)

加個修飾器:

@remote("192.168.10.2")
def foo(x):
return sum(range(x))

print foo(1000)

現在這個foo就能自動被扔到192.168.10.2上去算了,不需要動原來程序的任何東西。這算是個實際的栗子吧。

那個@remote實現大概是這樣的:

def remote(ip):
def wrapper(func):
def f_wrapped(*args, **kargs):
# serialising func, the arguments and the related environment.
# sending the packed function to the specified remote host
# waiting for the results
return results
return f_wrapped
return wrapper


可以做咖喱,可以寫裝飾器!


閉包從scheme開始就有了。所有支持lexical scoping以及first class function的語言都支持閉包。一般的decorator都是通過閉包實現的。


我只是覺得,有時候一個方法寫到一半,發現有些邏輯是可以「抽取到」另一個方法里完成的,並且會多次在方法內調用到,但是其中要用到的local變數太多,又不想一個個全傳過去,乾脆就寫個閉包吧。


為了讓python成為更優雅更可讀的語言,以及沒有了裝飾器你讓各種框架怎麼活。

最早接觸閉包是在寫js的時候,為了避免某些調皮的用戶改js數據。一開始有點難理解,但寫過幾次感覺別有洞天。如果對於編程的理解就止於寫個hello world,不如早點轉行。

並不是誰發明了閉包,只是語言的特性產生了這樣的寫法。


看了樓上的各種回答,感覺閉包的概念有點像c++中的帶參數的函數對象,為了引進外圍


是為了減輕寫程序的工作量,隨手寫個閉包就能非同步了


閉包傳遞上下文。

這個例子舉得不好,我加個條件,如果你想知道line_conf()被調用了幾次,你會怎麼寫?如果不用全局變數呢?這個時候你的寫法複雜程度肯定超過閉包。

如果你不用私有屬性定義全局變數,怎麼保證這個調用的次數不被調用你模塊的人直接訪問到?JS沒有類,就是用閉包做的這種事,簡單方便。


嗯,確實沒用,但是我看SICP這種精妙的代碼多的是。你可以看看SICP,就知道為什麼了。了。


等你有業務需求用到的時候就感覺好了。


我記得閉包是Lisp搞出來的~~挺有用的~閉包應該是函數式編程必備特性吧~


推薦閱讀:

學習python為什麼要在linux下?怎麼學?
NumPy和MATLAB哪個強大,Numpy能替代MATLAB嗎?
關於python遞歸的邏輯困惑?
Python出現ValueError: need more than 1 value to unpack 的原因是什麼?
有哪些應用場景適合用python的gevent來完成?

TAG:Python | 閉包 |