標籤:

Python 里為什麼函數可以返回一個函數內部定義的函數?

Python 初學。

看@函數修飾符的例子。看到一個這樣的例子

def spamrun(fn):
def sayspam(*args):
print "spam,spam,spam"
return sayspam

內部定義的函數可以作為返回值……內部這個函數的是不是被複制了一份然後返回給外部了……是不是可以這樣理解:

def spamrun(fn):
f = lambda args:print "spam,spam,spam"
return f

我想百度相關的知識的,找了半天沒有找到,「函數內部定義函數」這樣的關鍵字沒有找到想要的答案。求懂的人指導。

------------------------------------修改補充----------------------------

def addspam(fn):
def new(*args):
print "spam,spam,spam"
return fn(*args)
return new

@addspam
def useful(a,b):
print a**2+b**2

理解執行 useful(3, 4) 相當於:

useful(3,4) --&> addspam(useful(3,4))--&>

到這裡就不知道怎麼理解好了。addspam 返回了一個new,而 new 用到了addspam 傳入的參數,即 useful(3, 4),但是 new 的形參是怎麼給值的呢……所以 new 里執行的 fn(*args) 的 args 是怎麼給的。看輸出結果就是執行了 useful(3, 4)。只是不明白3,4怎麼就給到了 *args。


題主可能並沒有理解

「在Python中,函數本身也是對象」

這一本質。那不妨慢慢來,從最基本的概念開始,討論一下這個問題:

1. Python中一切皆對象

這恐怕是學習Python最有用的一句話。想必你已經知道Python中的list, tuple, dict等內置數據結構,當你執行:

alist = [1, 2, 3]

時,你就創建了一個列表對象,並且用alist這個變數引用它:

當然你也可以自己定義一個類:

class House(object):
def __init__(self, area, city):
self.area = area
self.city = city

def sell(self, price):
[...] #other code
return price

然後創建一個類的對象:

house = House(200, "Shanghai")

OK,你立馬就在上海有了一套200平米的房子,它有一些屬性(area, city),和一些方法(__init__, self):

2. 函數是第一類對象

和list, tuple, dict以及用House創建的對象一樣,當你定義一個函數時,函數也是對象:

def func(a, b):
return a+b

在全局域,函數對象被函數名引用著,它接收兩個參數a和b,計算這兩個參數的和作為返回值。

所謂第一類對象,意思是可以用標識符給對象命名,並且對象可以被當作數據處理,例如賦值、作為參數傳遞給函數,或者作為返回值return 等

因此,你完全可以用其他變數名引用這個函數對象:

add = func

這樣,你就可以像調用func(1, 2)一樣,通過新的引用調用函數了:

print func(1, 2)
print add(1, 2) #the same as func(1, 2)

或者將函數對象作為參數,傳遞給另一個函數:

def caller_func(f):
return f(1, 2)

if __name__ == "__main__":
print caller_func(func)

可以看到,

  • 函數對象func作為參數傳遞給caller_func函數,傳參過程類似於一個賦值操作f=func;

  • 於是func函數對象,被caller_func函數作用域中的局部變數f引用,f實際指向了函數func;cc

  • 當執行return f(1, 2)的時候,相當於執行了return func(1, 2);

因此輸出結果為3。

3. 函數對象 vs 函數調用

無論是把函數賦值給新的標識符,還是作為參數傳遞給新的函數,針對的都是函數對象本身,而不是函數的調用。

用一個更加簡單,但從外觀上看,更容易產生混淆的例子來說明這個問題。例如定義了下面這個函數:

def func():
return "hello,world"

然後分別執行兩次賦值:

ref1 = func #將函數對象賦值給ref1
ref2 = func() #調用函數,將函數的返回值("hello,world"字元串)賦值給ref2

很多初學者會混淆這兩種賦值,通過Python內建的type函數,可以查看一下這兩次賦值的結果:

In [4]: type(ref1)
Out[4]: function

In [5]: type(ref2)
Out[5]: str

可以看到,ref1引用了函數對象本身,而ref2則引用了函數的返回值。通過內建的callable函數,可以進一步驗證ref1是可調用的,而ref2是不可調用的:

In [9]: callable(ref1)
Out[9]: True

In [10]: callable(ref2)
Out[10]: False

傳參的效果與之類似。

4. 閉包LEGB法則

所謂閉包,就是將組成函數的語句和這些語句的執行環境打包在一起時,得到的對象

聽上去的確有些複雜,還是用一個栗子來幫助理解一下。假設我們在foo.py模塊中做了如下定義:

#foo.py
filename = "foo.py"

def call_func(f):
return f() #如前面介紹的,f引用一個函數對象,然後調用它

在另一個func.py模塊中,寫下了這樣的代碼:

#func.py
import foo #導入foo.py

filename = "func.py"
def show_filename():
return "filename: %s" % filename

if __name__ == "__main__":
print foo.call_func(show_filename) #注意:實際發生調用的位置,是在foo.call_func函數中

當我們用python func.py命令執行func.py時輸出結果為:

chiyu@chiyu-PC:~$ python func.py
filename:func.py

很顯然show_filename()函數使用的filename變數的值,是在與它相同環境(func.py模塊)中定義的那個。儘管foo.py模塊中也定義了同名的filename變數,而且實際調用show_filename的位置也是在foo.py的call_func內部

而對於嵌套函數,這一機制則會表現的更加明顯:閉包將會捕捉內層函數執行所需的整個環境

#enclosed.py
import foo
def wrapper():
filename = "enclosed.py"
def show_filename():
return "filename: %s" % filename
print foo.call_func(show_filename) #輸出:filename: enclosed.py

實際上,每一個函數對象,都有一個指向了該函數定義時所在全局名稱空間的__globals__屬性:

#show_filename inside wrapper
#show_filename.__globals__

{
"__builtins__": &, #內建作用域環境
"__file__": "enclosed.py",
"wrapper": &, #直接外圍環境
"__package__": None,
"__name__": "__main__",
"foo": &, #全局環境
"__doc__": None
}

當代碼執行到show_filename中的return "filename: %s" % filename語句時,解析器按照下面的順序查找filename變數:

  • Local - 本地函數(show_filename)內部,通過任何方式賦值的,而且沒有被global關鍵字聲明為全局變數的filename變數;

  • Enclosing - 直接外圍空間(上層函數wrapper)的本地作用域,查找filename變數(如果有多層嵌套,則由內而外逐層查找,直至最外層的函數);

  • Global - 全局空間(模塊enclosed.py),在模塊頂層賦值的filename變數;

  • Builtin - 內置模塊(__builtin__)中預定義的變數名中查找filename變數;

在任何一層先找到了符合要求的filename變數,則不再向更外層查找。如果直到Builtin層仍然沒有找到符合要求的變數,則拋出NameError異常。這就是變數名解析的:LEGB法則。

總結:

  1. 閉包最重要的使用價值在於:封存函數執行的上下文環境

  2. 閉包在其捕捉的執行環境(def語句塊所在上下文)中,也遵循LEGB規則逐層查找,直至找到符合要求的變數,或者拋出異常。

5. 裝飾器語法糖(syntax sugar)

那麼閉包和裝飾器又有什麼關係呢?

上文提到閉包的重要特性:封存上下文,這一特性可以巧妙的被用於現有函數的包裝,從而為現有函數更加功能。而這就是裝飾器。

還是舉個例子,代碼如下:

#alist = [1, 2, 3, ..., 100] --&> 1+2+3+...+100 = 5050
def lazy_sum():
return reduce(lambda x, y: x+y, alist)

我們定義了一個函數lazy_sum,作用是對alist中的所有元素求和後返回。alist假設為1到100的整數列表:

alist = range(1, 101)

但是出於某種原因,我並不想馬上返回計算結果,而是在之後的某個地方,通過顯示的調用輸出結果。於是我用一個wrapper函數對其進行包裝:

def wrapper():
alist = range(1, 101)
def lazy_sum():
return reduce(lambda x, y: x+y, alist)
return lazy_sum

lazy_sum = wrapper() #wrapper() 返回的是lazy_sum函數對象

if __name__ == "__main__":
lazy_sum() #5050

這是一個典型的Lazy Evaluation的例子。我們知道,一般情況下,局部變數在函數返回時,就會被垃圾回收器回收,而不能再被使用。但是這裡的alist卻沒有,它隨著lazy_sum函數對象的返回被一併返回了(這個說法不準確,實際是包含在了lazy_sum的執行環境中,通過__globals__),從而延長了生命周期。

當在if語句塊中調用lazy_sum()的時候,解析器會從上下文中(這裡是Enclosing層的wrapper函數的局部作用域中)找到alist列表,計算結果,返回5050。

當你需要動態的給已定義的函數增加功能時,比如:參數檢查,類似的原理就變得很有用:

def add(a, b):
return a+b

這是很簡單的一個函數:計算a+b的和返回,但我們知道Python是 動態類型+強類型 的語言,你並不能保證用戶傳入的參數a和b一定是兩個整型,他有可能傳入了一個整型和一個字元串類型的值:

In [2]: add(1, 2)
Out[2]: 3

In [3]: add(1.2, 3.45)
Out[3]: 4.65

In [4]: add(5, "hello")
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/home/chiyu/& in &()
----&> 1 add(5, "hello")

/home/chiyu/& in add(a, b)
1 def add(a, b):
----&> 2 return a+b

TypeError: unsupported operand type(s) for +: "int" and "str"

於是,解析器無情的拋出了一個TypeError異常。

動態類型:在運行期間確定變數的類型,python確定一個變數的類型是在你第一次給他賦值的時候;

強類型:有強制的類型定義,你有一個整數,除非顯示的類型轉換,否則絕不能將它當作一個字元串(例如直接嘗試將一個整型和一個字元串做+運算);

因此,為了更加優雅的使用add函數,我們需要在執行+運算前,對a和b進行參數檢查。這時候裝飾器就顯得非常有用:

import logging

logging.basicConfig(level = logging.INFO)

def add(a, b):
return a + b

def checkParams(fn):
def wrapper(a, b):
if isinstance(a, (int, float)) and isinstance(b, (int, float)): #檢查參數a和b是否都為整型或浮點型
return fn(a, b) #是則調用fn(a, b)返回計算結果

#否則通過logging記錄錯誤信息,並友好退出
logging.warning("variable "a" and "b" cannot be added")
return
return wrapper #fn引用add,被封存在閉包的執行環境中返回

if __name__ == "__main__":
#將add函數對象傳入,fn指向add
#等號左側的add,指向checkParams的返回值wrapper
add = checkParams(add)
add(3, "hello") #經過類型檢查,不會計算結果,而是記錄日誌並退出

注意checkParams函數:

  • 首先看參數fn,當我們調用checkParams(add)的時候,它將成為函數對象add的一個本地(Local)引用;

  • 在checkParams內部,我們定義了一個wrapper函數,添加了參數類型檢查的功能,然後調用了fn(a, b),根據LEGB法則,解釋器將搜索幾個作用域,並最終在(Enclosing層)checkParams函數的本地作用域中找到fn;

  • 注意最後的return wrapper,這將創建一個閉包,fn變數(add函數對象的一個引用)將會封存在閉包的執行環境中,不會隨著checkParams的返回而被回收;

當調用add = checkParams(add)時,add指向了新的wrapper對象,它添加了參數檢查和記錄日誌的功能,同時又能夠通過封存的fn,繼續調用原始的add進行+運算。

因此調用add(3, "hello")將不會返回計算結果,而是列印出日誌:

chiyu@chiyu-PC:~$ python func.py
WARNING:root:variable "a" and "b" cannot be added

有人覺得add = checkParams(add)這樣的寫法未免太過麻煩,於是python提供了一種更優雅的寫法,被稱為語法糖:

@checkParams
def add(a, b):
return a + b

這只是一種寫法上的優化,解釋器仍然會將它轉化為add = checkParams(add)來執行。

6. 回歸問題

def addspam(fn):
def new(*args):
print "spam,spam,spam"
return fn(*args)
return new

@addspam
def useful(a,b):
print a**2+b**2

首先看第二段代碼:

  • @addspam裝飾器,相當於執行了useful = addspam(useful)。在這裡題主有一個理解誤區:傳遞給addspam的參數,是useful這個函數對象本身,而不是它的一個調用結果;

再回到addspam函數體:

  • return new 返回一個閉包,fn被封存在閉包的執行環境中,不會隨著addspam函數的返回被回收;

  • 而fn此時是useful的一個引用,當執行return fn(*args)時,實際相當於執行了return useful(*args);

最後附上一張代碼執行過程中的引用關係圖,希望能幫助你理解:


題主遇到的這個問題很典型,就是把修飾器當成了包裝器, 認為調用 useful 前先調用 addspam。其實 useful 不是被 addspam 包裝了,而是替換了。調用 useful 調用的就是 new。餘下的 fn 指向原來的 useful, 才需要第一類函數、作用域繼承等知識點來理解。可變參數 *args 則是干擾的另外一個知識點,替換成 x, y 就行了。


你沒理解修飾器。

用addspam修飾了useful後,你應該理解為這個函數變成了new。

當調用useful函數的時候,其實是調用了new。


很好的問題!

請搜索higher order functions了解更多信息。

因為我沒有能力來清楚地解釋,就放幾篇我在學習函數式編程的時候看到的比較好的博文來幫助你理解。

函數式編程 | 酷 殼

Python修飾器的函數式編程

1.6 Higher-Order Functions

6. Functional Programming

我喜歡最後一篇文章,通過實例來理解效果更好。


不是簡單地返回函數。至少在Python里,def定義的函數和lambda定義的函數,後者是包含closure的。

具體closure是什麼,這真不是一句話能說清,我也不覺得我能說好,所以還是自己搜一下吧。

不要說我歧視用百度查這種問題,這去Google搜個nested functions多好。


沒有複製,函數也是個對象,基本就和你 return 一個 list 一個 dict 沒什麼兩樣。

試試看在 Python REPL 中創建一個 function:

&>&>&> def foobar(): print("你好")
&>&>&> foobar
&>&>&> func_list = [foobar, foobar, foobar]
&>&>&> func_list[0]()

後者是一個閉包 ( closure ),簡單來說就是函數對象中包裝了函數中引用的外部變數,可以想像成這個函數被動態創建的時候,引用的外部變數凍結在函數裡面了。

你新補充的我沒怎麼看懂,*args 的作用嗎?*args 在形參上的作用類似捕獲給函數的實參放在一個 args 的表中作為形參,如果作為實參傳入的話,就是將 args 這個表解開作為分別的形參輸入。


Python裡面的@只是一個syntax sugar而已,在你聲明useful的時候,interpreter檢查到你有外面有裝飾器@addspam的存在,這時候你就可以大致理解成解釋器做了以下的手腳:

useful = addspam(useful)

所以你以後調用useful的時候,你調用的其實是new,不信可以看一下這個useful.__name__,已經變成new了。到了這裡就沒有addspam什麼事情了。


高票已經講得很好了。我補充一點從Python虛擬機的角度來看這個問題的一些信息。

在Python中,有三種對象與方法的定義有關,一種是PyCodeObject,它代表的是靜態的位元組碼;還有一種是PyFunctionObject,它代表的是執行過程中動態生成的函數對象;還有一種是PyMethodObject,它代表的是class中定義的方法。先不去管PyMethodObject,我們先看PyCodeObject和PyFunctionObject。

還是用我在篇回答里的辦法:海納:函數內部的變數在函數執行完後就銷毀,為什麼可變對象卻能保存上次調用時的結果呢??

查看一下位元組碼,比如我們保存一個deco.py文件如下:

def getFoo():
def foo():
return "hello world"

return foo

f = getFoo()
print f()

然後通過REPL查看getFoo的位元組碼:

&>&>&> import dis
&>&>&> import deco
&>&>&> dis.dis(deco.getFoo)
2 0 LOAD_CONST 1 (&)
3 MAKE_FUNCTION 0
6 STORE_FAST 0 (foo)

5 9 LOAD_FAST 0 (foo)
12 RETURN_VALUE

可以看到,在getFoo里就是載入了一個code object foo,然後通過這個CodeObject創建了一個FunctionObject。然後把這個FunctionObject賦值給 foo 變數,然後再把它做為返回值return出來。

為什麼要有FunctionObject呢?這是因為一個函數可能還有默認參數,閉包參數等,必須要依靠運行時的環境的內容。所以Python就使用了代表靜態代碼的CodeObject和代表動態方法的FunctionObject。

好了。到這裡為止,就差不多可以理解為什麼函數可以做為值返回了,因為在Python中,函數也是一種對象。支持這麼做的語言,我們把它稱為「函數是第一類公民(first class)」,意思是,在這種語言里,函數是可以像int, float等基本類型一樣做為參數傳遞,也可以做為返回值返回的。

好了。再說decorator,很多人都沒有理解decorator的本質,各種說法滿天飛。其實decorator很簡單的。它是一個語法糖(就是一種寫法等價於另一種寫法,但帶上糖寫起來更快更爽):

@deco
def foo():
return 1

與以下代碼是完全等價的:

def foo():
return 1

foo = deco(foo)

我們來看一下,decorator寫法的位元組碼:

1 0 LOAD_CONST 0 (&)
3 MAKE_FUNCTION 0
6 STORE_NAME 0 (deco)

4 9 LOAD_NAME 0 (deco)
12 LOAD_CONST 1 (&)
15 MAKE_FUNCTION 0
18 CALL_FUNCTION 1
21 STORE_NAME 1 (foo)
24 LOAD_CONST 2 (None)
27 RETURN_VALUE

注意位元組碼編號為18的那一條,CALL_FUNCTION 1,以棧頂的第一個對象為參數,調用第二個方法對象。講人話就是deco(foo),呵呵,然後這個返回值,又賦值給了foo。好了,原來只不過就是

foo = deco(foo)

而已嘛。

為節省篇幅,非decorator寫法的位元組碼我就不貼了,大家可以自己嘗試一下。比較一下這兩種寫法的位元組碼。


舉個例子:

# block 1
1 def test(f):
2 def _test():
3 print "..."
4 return _test

# block 2
5 @test
6 def g():
7 pass

8 if __name__ == "__main__":
9 g()

為了方便講解,特意人肉加上行號。

給個最簡單的方式去理解裝飾器吧。

我們先看block 1的代碼,假設這個嵌套函數執行,運行順序是1-&>4,對,只到4!因為整個block 1返回的是_test函數,沒錯,簡單的說就是執行test(f),返回_test函數,因為返回的是函數而不是函數調用,所以3是不會執行的。如果還是覺得費解,我們不妨測試一下

def test(f):
def _test():
print "..."
return _test

def f():
pass

if __name__ == "__main__":
print test(f).__name__
print type(test(f))

這裡輸出兩個結果,第一個是調用test函數時返回的是哪個函數,第二個是輸出返回的類型,可以看到結果是

_test
&

這樣就能理解了,為什麼我調用了函數test(f),但是卻沒有輸出"..."。

然後看block2的代碼,這個其實等於執行的是g=test(g),你可以把這個當成語法糖的等式。那就是說,block2得到的是_test函數。

最後我們可以來看main的調用了。我們看到執行的是g(),相當於執行的是test(g)的調用,而test(g)返回的是_test,也就相當於執行的是_test的調用了。


裝飾器 - 廖雪峰的官方網站 看完就明白了


3. Data model - Python 3.6.4 documentationdocs.python.org

且看官方文檔的對象模型,可調用對象在 Python中實際上是「一等對象」,和所謂的整數 None 有著同樣的地位。那麼,我們可以反悔自己定義的對象 可以返回整數 可以返回None,我們也可以返回在這個意義上對等的「函數」吧。


不錯 看完就明白了


推薦閱讀:

為何大量設計模式在動態語言中不適用?
python如何繪製一個橫坐標為字元串,縱坐標為數字的折線圖?
python 的 dict真的不會隨著key的增加而變慢嗎?
為什麼要學 Python?
有哪些值得推薦的Python學習網站?

TAG:Python |