Python函式是所謂的第一類物件(First-Class Object)是什麼鬼?

劉志軍發表於2017-04-15

之前寫過一篇關於裝飾器的文章,雖然寫得還算不錯,但是也有不少同學表示沒看懂,我大概分析了其中的原因,主要問題是他們不理解函式,因為Python中的函式不同於其它語言。

正確理解 Python函式,能夠幫助我們更好地理解 Python 裝飾器、匿名函式(lambda)、函數語言程式設計等高階技術。

函式(Function)作為程式語言中不可或缺的一部分,太稀鬆平常了。但函式作為第一類物件(First-Class Object)卻是 Python 函式的一大特性。那到底什麼是第一類物件呢?

函式是物件

在 Python 中萬物皆為物件,函式也不例外,函式作為物件可以賦值給一個變數、可以作為元素新增到集合物件中、可作為引數值傳遞給其它函式,還可以當做函式的返回值,這些特性就是第一類物件所特有的。

先來看一個簡單的例子

>>> def foo(text):
...     return len(text)
...
>>> foo("zen of python")
13複製程式碼

這是一個再簡單不過的函式,用於計算引數 text 的長度,呼叫函式就是函式名後面跟一個括號,再附帶一個引數,返回值是一個整數。

函式身為一個物件,擁有物件模型的三個通用屬性:id、型別、和值。

>>> id(foo)
4361313816
>>> type(foo)
<class 'function'>
>>> foo
<function foo at 0x103f45e18>複製程式碼

作為物件,函式可以賦值給一個變數

>>> bar = foo複製程式碼

Python函式是所謂的第一類物件(First-Class Object)是什麼鬼?

賦值給另外一個變數時,函式並不會被呼叫,僅僅是在函式物件上繫結一個新的名字而已。

>>> bar("zen of python")
13
>>>複製程式碼

同理,你還可以把該函式賦值給更多的變數,唯一變化的是該函式物件的引用計數不斷地增加,本質上這些變數最終指向的都是同一個函式物件。

>>> a = foo
>>> b = foo
>>> c = bar
>>> a is b is c
True複製程式碼

函式可以儲存在容器

容器物件(list、dict、set等)中可以存放任何物件,包括整數、字串,函式也可以作存放到容器物件中,例如

>>> funcs = [foo, str, len]
>>> funcs
[<function foo at 0x103f45e18>, <class 'str'>, <built-in function len>]
>>> for f in funcs:
...     print(f("hello"))
...
5
hello
5
>>>複製程式碼

foo 是我們自定義的函式,str 和 len 是兩個內建函式。for 迴圈逐個地迭代出列表中的每個元素時,函式物件賦值給了 f 變數,呼叫 f(“hello”) 與 呼叫 foo(“hello”) 本質是一樣的效果,每次 f 都重新指向一個新的函式物件。當然,你也可以使用列表的索引定位到元素來呼叫函式。

>>> funcs[0]("Python之禪")
# 等效於 foo("Python之禪")
8複製程式碼

函式可以作為引數

函式還可以作為引數值傳遞給另外一個函式,例如:

>>> def show(func):
...     size = func("python 之禪") # 等效於 foo("Python之禪") 
...     print ("length of string is : %s" % size)
...
>>> show(foo)
length of string is : 9複製程式碼

函式可以作為返回值

函式作為另外一個函式的返回值,例如:

>>> def nick():
...     return foo
>>> nick
<function nick at 0x106b549d8>
>>> a = nick()
>>> a
<function foo at 0x10692ae18>
>>> a("python")
6複製程式碼

還可以簡寫為

>>> nick()("python")
6複製程式碼

函式接受一個或多個函式作為輸入或者函式輸出(返回)的值是函式時,我們稱這樣的函式為高階函式,比如上面的 shownick 都屬於高階函式。

Python內建函式中,典型的高階函式是 map 函式,map 接受一個函式和一個迭代物件作為引數,呼叫 map 時,依次迭代把迭代物件的元素作為引數呼叫該函式。

>>> map(foo, ["the","zen","of","python"])
>>> lens = map(foo, ["the","zen","of","python"])
>>> list(lens)
[3, 3, 2, 6]複製程式碼

map 函式的作用相當於:

>>> [foo(i) for i in ["the","zen","of","python"]]
[3, 3, 2, 6]複製程式碼

只不過 map 的執行效率更快一點。

函式可以巢狀

Python還允許函式中定義函式,這種函式叫巢狀函式。

>>> def get_length(text):
...     def clean(t):           # 2
...         return t[1:]
...     new_text = clean(text)  # 1
...     return len(new_text)
...
>>> get_length("python")
5
>>>複製程式碼

這個函式的目的是去除字串的第一個字元後再計算它的長度,儘管函式本身的意義不大,但能足夠說明巢狀函式。get_length 呼叫時,先執行1處程式碼,發現有呼叫 clean 函式,於是接著執行2中的程式碼,把返回值賦值給了 new_text ,再繼續執行後續程式碼。

>>> clean("python")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'clean' is not defined複製程式碼

函式中裡面巢狀的函式不能在函式外面訪問,只能是在函式內部使用,超出了外部函式的做用域就無效了。

實現了 __call__ 的類也可以作為函式

對於一個自定義的類,如果實現了 __call__ 方法,那麼該類的例項物件的行為就是一個函式,是一個可以被呼叫(callable)的物件。例如:

class Add:
    def __init__(self, n):
         self.n = n
    def __call__(self, x):
        return self.n + x

>>> add = Add(1)
>>> add(4)
>>> 5複製程式碼

執行 add(4) 相當於呼叫 Add._call__(add, 4),self 就是例項物件 add,self.n 等於 1,所以返回值為 1+4

add(4)
  ||
Add(1)(4)
  ||
Add.__call__(add, 4)複製程式碼

確定物件是否為可呼叫物件可以用內建函式callable來判斷。

>>> callable(foo)
True
>>> callable(1)
False
>>> callable(int)
True複製程式碼

總結

Python中包含函式在內的一切皆為物件,函式作為第一類物件,支援賦值給變數,作為引數傳遞給其它函式,作為其它函式的返回值,支援函式的巢狀,實現了__call__方法的類例項物件也可以當做函式被呼叫。

這兩天我在公眾號 Python之禪 (id:VTtalk)發起了一次送圖書的福利活動,您可以去看看

同步發表部落格:foofish.net/function-is…

Python函式是所謂的第一類物件(First-Class Object)是什麼鬼?
Python之禪

相關文章