當編譯器遇到 def,會生成創(chuàng)建函數(shù)對象指令。也就是說 def 是執(zhí)行指令,而不僅僅是個語法關(guān)鍵字??梢栽谌魏蔚胤絼討B(tài)創(chuàng)建函數(shù)對象。
一個完整的函數(shù)對象由函數(shù)和代碼兩部分組成。其中,PyCodeObject 包含了字節(jié)碼等執(zhí)行數(shù)據(jù),而 PyFunctionObject 則為其提供了狀態(tài)信息。
函數(shù)聲明:
def name([arg,... arg = value,... *arg, **kwarg]):
suite
結(jié)構(gòu)定義:
typedef struct {
PyObject_HEAD
PyObject *func_code; // PyCodeObject
PyObject *func_globals; // 所在模塊的全局名字空間
PyObject *func_defaults; // 參數(shù)默認值列表
PyObject *func_closure; // 閉包列表
PyObject *func_doc; // __doc__
PyObject *func_name; // __name__
PyObject *func_dict; // __dict__
PyObject *func_weakreflist; // 弱引用鏈表
PyObject *func_module; // 所在 Module
} PyFunctionObject;
包括函數(shù)在內(nèi)的所有對象都是第一類對象,可作為其他函數(shù)的實參或返回值。
>>> def test(name):
... if name == "a":
... def a(): pass
... return a
... else:
... def b(): pass
... return b
>>> test("a").__name__
'a'
不同于用 def 定義復(fù)雜函數(shù),lambda 只能是有返回值的簡單的表達式。使用賦值語句會引發(fā)語法錯誤,可以考慮用函數(shù)代替。
>>> add = lambda x, y = 0: x + y
>>> add(1, 2)
3
>>> add(3) # 默認參數(shù)
3
>>> map(lambda x: x % 2 and None or x, range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
函數(shù)的傳參方式靈活多變,可按位置順序傳參,也可不關(guān)心順序用命名實參。
>>> def test(a, b):
... print a, b
>>> test(1, "a") # 位置參數(shù)
1 a
>>> test(b = "x", a = 100) # 命名參數(shù)
100 x
支持參數(shù)默認值。不過要小心,默認值對象在創(chuàng)建函數(shù)時生成,所有調(diào)用都使用同一對象。如果該默認值是可變類型,那么就如同 C 靜態(tài)局部變量。
>>> def test(x, ints = []):
... ints.append(x)
... return ints
>>> test(1)
[1]
>>> test(2) # 保持了上次調(diào)用狀態(tài)。
[1, 2]
>>> test(1, []) # 顯式提供實參,不使用默認值。
[1]
>>> test(3) # 再次使用默認值。
[1, 2, 3]
默認參數(shù)后面不能有其他位置參數(shù),除非是變參。
>>> def test(a, b = 0, c): pass
SyntaxError: non-default argument follows default argument
>>> def test(a, b = 0, *args, **kwargs): pass
用 *args 收集 "多余" 的位置參數(shù),**kwargs 收集 "額外" 的命名參數(shù)。這兩個名字只是慣例,可自由命名。
>>> def test(a, b, *args, **kwargs):
... print a, b
... print args
... print kwargs
>>> test(1, 2, "a", "b", "c", x = 100, y = 200)
1 2
('a', 'b', 'c')
{'y': 200, 'x': 100}
變參只能放在所有參數(shù)定義的尾部,且 **kwargs 必須是最后一個。
>>> def test(*args, **kwargs): # 可以接收任意參數(shù)的函數(shù)。
... print args
... print kwargs
>>> test(1, "a", x = "x", y = "y") # 位置參數(shù),命名參數(shù)。
(1, 'a')
{'y': 'y', 'x': 'x'}
>>> test(1) # 僅傳位置參數(shù)。
(1,)
{}
>>> test(x = "x") # 僅傳命名參數(shù)。
()
{'x': 'x'}
可 "展開" 序列類型和字典,將全部元素當做多個實參使用。如不展開的話,那僅是單個實參對象。
>>> def test(a, b, *args, **kwargs):
... print a, b
... print args
... print kwargs
>>> test(*range(1, 5), **{"x": "Hello", "y": "World"})
1 2
(3, 4)
{'y': 'World', 'x': 'Hello'}
單個 "*" 展開序列類型,或者僅是字典的主鍵列表。"**" 展開字典鍵值對。但如果沒有變參收集,展開后多余的參數(shù)將引發(fā)異常。
>>> def test(a, b):
... print a
... print b
>>> d = dict(a = 1, b = 2)
>>> test(*d) # 僅展開 keys(),test("a"、"b")。
a
b
>>> test(**d) # 展開 items(),test(a = 1, b = 2)。
1
2
>>> d = dict(a = 1, b = 2, c = 3)
>>> test(*d) # 因為沒有位置變參收集多余的 "c",導(dǎo)致出錯。
TypeError: test() takes exactly 2 arguments (3 given)
>>> test(**d) # 因為沒有命名變參收集多余的 "c = 3",導(dǎo)致出錯。
TypeError: test() got an unexpected keyword argument 'c'
lambda 同樣支持默認值和變參,使用方法完全一致。
>>> test = lambda a, b = 0, *args, **kwargs: \
... sum([a, b] + list(args) + kwargs.values())
>>> test(1, *[2, 3, 4], **{"x": 5, "y": 6})
21
函數(shù)形參和內(nèi)部變量都存儲在 locals 名字空間中。
>>> def test(a, *args, **kwargs):
... s = "Hello, World"
... print locals()
>>> test(1, "a", "b", x = 10, y = "hi")
{
'a': 1,
'args': ('a', 'b'),
'kwargs': {'y': 'hi', 'x': 10}
's': 'Hello, World',
}
除非使用 global、nonlocal 特別聲明,否則在函數(shù)內(nèi)部使用賦值語句,總是在 locals 名字空間中新建一個對象關(guān)聯(lián)。注意:"賦值" 是指名字指向新的對象,而非通過名字改變對象狀態(tài)。
>>> x = 10
>>> hex(id(x))
'0x7fb8e04105e0'
>>> def test():
... x = "hi"
... print hex(id(x)), x
>>> test() # 兩個 x 指向不同的對象。
0x10af2b490 hi
>>> x # 外部變量沒有被修改。
10
如果僅僅是引用外部變量,那么按 LEGB 順序在不同作用域查找該名字。
名字查找順序: locals -> enclosing function -> globals -> __builtins__
>>> __builtins__.b = "builtins"
>>> g = "globals"
>>> def enclose():
... e = "enclosing"
... def test():
... l = "locals"
... print l
... print e
... print g
... print b
...
... return test
>>> t = enclose()
>>> t()
locals
enclosing
globals
builtins
通常內(nèi)置模塊 builtin 在本地名字空間的名字是 builtins (多了個 s 結(jié)尾)。但要記住這說法一點也不靠譜,某些時候它又會莫名其妙地指向 builtin.dict。如實在要操作該模塊,建議顯式 import builtin。
27.3. __builtin__ — Built-in objects
CPython implementation detail: Most modules have the name __builtins__ (note the 's') made available as partof their globals. The value of __builtins__ is normally either this module or the value of this modules’s __dict__attribute. Since this is an implementation detail, it may not be used by alternate implementations of Python.
現(xiàn)在,獲取外部空間的名字沒問題了,但如果想將外部名字關(guān)聯(lián)到一個新對象,就需要使用 global關(guān)鍵字,指明要修改的是 globals 名字空間。Python 3 還提供了 nonlocal 關(guān)鍵字,用來修改外部嵌套函數(shù)名字空間,可惜 2.7 沒有。
>>> x = 100
>>> hex(id(x))
0x7f9a9264a028
>>> def test():
... global x, y # 聲明 x, y 是 globals 名字空間中的。
... x = 1000 # globals()["x"] = 1000
... y = "Hello, World" # globals()["y"] = "..."。 新建名字。
... print hex(id(x))
>>> test() # 可以看到 test.x 引用的是外部變量 x。
0x7fdfba4abb30
>>> print x, hex(id(x)) # x 被修改。外部 x 指向新整數(shù)對象 1000。
1000 0x7fdfba4abb30
>>> x, y # globals 名字空間中出現(xiàn)了 y。
(1000, 'Hello, World')
沒有 nonlocal 終歸有點不太方便,要實現(xiàn)類似功能稍微有點麻煩。
>>> from ctypes import pythonapi, py_object
>>> from sys import _getframe
>>> def nonlocal(**kwargs):
... f = _getframe(2)
... ns = f.f_locals
... ns.update(kwargs)
... pythonapi.PyFrame_LocalsToFast(py_object(f), 0)
>>> def enclose():
... x = 10
...
... def test():
... nonlocal(x = 1000)
...
... test()
... print x
>>> enclose()
1000
這種實現(xiàn)通過 _getframe() 來獲取外部函數(shù)堆棧幀名字空間,存在一些限制。因為拿到是調(diào)用者,而不一定是函數(shù)創(chuàng)建者。
需要注意,名字作用域是在編譯時確定的。比如下面例子的結(jié)果,會和設(shè)想的有很大差異。究其原因,是編譯時并不存在 locals x 這個名字。
>>> def test():
... locals()["x"] = 10
... print x
>>> test()
NameError: global name 'x' is not defined
要解決這個問題,可動態(tài)訪問名字,或使用 exec 語句,解釋器會做動態(tài)化處理。
>>> def test():
... exec "" # 空語句。
... locals()["x"] = 10
... print x
>>> test()
10
>>> def test():
... exec "x = 10" # exec 默認使用當前名字空間。
... print x
>>> test()
10
如果函數(shù)中包含 exec 語句,編譯器生成的名字指令會依照 LEGB 規(guī)則搜索。繼續(xù)看下面的例子。
>>> x = "abc"
>>> def test():
... print x
... exec "x = 10"
... print x
>>> test()
abc
10
解釋器會將 locals 名字復(fù)制到 FAST 區(qū)域來優(yōu)化訪問速度,因此直接修改 locals 名字空間并不會影響該區(qū)域。解決方法還是用 exec。
>>> def test():
... x = 10
...
... locals()["x"] = 100 # 該操作不會影響 FAST 區(qū)域,只不過指向一個新對象。
... print x # 使用 LOAD_FAST 訪問 FAST 區(qū)域名字,依然是原對象。
...
... exec "x = 100" # 同時刷新 locals 和 FAST。
... print x
>>> test()
10
100
另外,編譯期作用域不受執(zhí)行期條件影響。
>>> def test():
... if False:
... global x # 盡管此語句永不執(zhí)行,但編譯器依然會將 x 當做 globals 名字。
... x = 10
... print globals()["x"] is x
>>> test()
True
>>> x
10
>>> def test():
... if False:
... x = 10 # 同理,x 是 locals 名字。后面出錯也就很正常了。
... print x
>>> test()
UnboundLocalError: local variable 'x' referenced before assignment
其中細節(jié),可以用 dis 反編譯查看生成的字節(jié)指令。
閉包是指:當函數(shù)離開創(chuàng)建環(huán)境后,依然持有其上下文狀態(tài)。比如下面的 a 和 b,在離開 test 函數(shù)后,依然持有 test.x 對象。
>>> def test():
... x = [1, 2]
... print hex(id(x))
...
... def a():
... x.append(3)
... print hex(id(x))
...
... def b():
... print hex(id(x)), x
...
... return a, b
>>> a, b = test()
0x109b925a8 # test.x
>>> a()
0x109b925a8 # 指向 test.x
>>> b()
0x109b925a8 [1, 2, 3]
實現(xiàn)方式很簡單,以上例來解釋:
test 在創(chuàng)建 a 和 b 時,將它們所引用的外部對象 x 添加到 func_closure 列表中。因為 x 引用計數(shù)增加了,所以就算 test 堆棧幀沒有了,x 對象也不會被回收。
>>> a.func_closure
(<cell at 0x109e0aef8: list object at 0x109b925a8>,)
>>> b.func_closure
(<cell at 0x109e0aef8: list object at 0x109b925a8>,)
為什么用 function.func_closure,而不是堆棧幀的名字空間呢?那是因為 test 僅僅返回兩個函數(shù)對象,并沒有調(diào)用它們,自然不可能為它們創(chuàng)建堆棧幀。這樣一來,就導(dǎo)致每次返回的 a 和 b 都是新建對象,否則這個閉包狀態(tài)就被覆蓋了。
>>> def test(x):
... def a():
... print x
...
... print hex(id(a))
... return a
>>> a1 = test(100) # 每次創(chuàng)建 a 都提供不同的參數(shù)。
0x109c700c8
>>> a2 = test("hi") # 可以看到兩次返回的函數(shù)對象并不相同。
0x109c79f50
>>> a1() # a1 的狀態(tài)沒有被 a2 破壞。
100
>>> a2()
hi
>>> a1.func_closure # a1、a2 持有的閉包列表是不同的。
(<cell at 0x109e0cf30: int object at 0x7f9a92410ce0>,)
>>> a2.func_closure
(<cell at 0x109d3ead0: str object at 0x109614490>,)
>>> a1.func_code is a2.func_code # 這個很好理解,字節(jié)碼沒必要有多個。
True
通過 func_code,可以獲知閉包所引用的外部名字。
>>> test.func_code.co_cellvars # 被內(nèi)部函數(shù) a 引用的名字。
('x',)
>>> a.func_code.co_freevars # a 引用外部函數(shù) test 中的名字。
('x',)
使用閉包,還需注意 "延遲獲取" 現(xiàn)象??聪旅娴睦樱?/p>
>>> def test():
... for i in range(3):
... def a():
... print i
... yield a
>>> a, b, c = test()
>>> a(), b(), c()
2
2
2
為啥輸出的都是 2 呢?
首先,test 只是返回函數(shù)對象,并沒有執(zhí)行。其次,test 完成 for 循環(huán)時,i 已經(jīng)等于 2,所以執(zhí)行 a、b、c 時,它們所持有 i 自然也就等于 2。
Python 堆棧幀基本上就是對 x86 的模擬,用指針對應(yīng) BP、SP、IP 寄存器。堆棧幀成員包括函數(shù)執(zhí)行所需的名字空間、調(diào)用堆棧鏈表、異常狀態(tài)等。
typedef struct _frame {
PyObject_VAR_HEAD
struct _frame *f_back; // 調(diào)用堆棧 (Call Stack) 鏈表
PyCodeObject *f_code; // PyCodeObject
PyObject *f_builtins; // builtins 名字空間
PyObject *f_globals; // globals 名字空間
PyObject *f_locals; // locals 名字空間
PyObject **f_valuestack; // 和 f_stacktop 共同維護運行幀空間,相當于 BP 寄存器。
PyObject **f_stacktop; // 運行棧頂,相當于 SP 寄存器的作用。
PyObject *f_trace; // Trace function
PyObject *f_exc_type, *f_exc_value, *f_exc_traceback; // 記錄當前棧幀的異常信息
PyThreadState *f_tstate; // 所在線程狀態(tài)
int f_lasti; // 上一條字節(jié)碼指令在 f_code 中的偏移量,類似 IP 寄存器。
int f_lineno; // 與當前字節(jié)碼指令對應(yīng)的源碼行號
... ...
PyObject *f_localsplus[1]; // 動態(tài)申請的一段內(nèi)存,用來模擬 x86 堆棧幀所在內(nèi)存段。
} PyFrameObject;
可使用 sys._getframe(0) 或 inspect.currentframe() 獲取當前堆棧幀。其中 _getframe() 深度參數(shù)為 0 表示當前函數(shù),1 表示調(diào)用堆棧的上個函數(shù)。除用于調(diào)試外,還可利用堆棧幀做些有意思的事情。
權(quán)限管理
通過調(diào)用堆棧檢查函數(shù) Caller,以實現(xiàn)權(quán)限管理。
>>> def save():
... f = _getframe(1)
... if not f.f_code.co_name.endswith("_logic"): # 檢查 Caller 名字,限制調(diào)用者身份。
... raise Exception("Error") # 還可以檢查更多信息。
... print "ok"
>>> def test(): save()
>>> def test_logic(): save()
>>> test()
Exception: Error
>>> test_logic()
ok
上下文
通過調(diào)用堆棧,我們可以隱式向整個執(zhí)行流程傳遞上下文對象。 inspect.stack 比 frame.f_back更方便一些。
>>> import inspect
>>> def get_context():
... for f in inspect.stack(): # 循環(huán)調(diào)用堆棧列表。
... context = f[0].f_locals.get("context") # 查看該堆棧幀名字空間中是否有目標。
... if context: return context # 找到了就返回,并終止查找循環(huán)。
>>> def controller():
... context = "ContextObject" # 將 context 添加到 locals 名字空間。
... model()
>>> def model():
... print get_context() # 通過調(diào)用堆棧查找 context。
>>> controller() # 測試通過。
ContextObject
sys._current_frames 返回所有線程的當前堆棧幀對象。
虛擬機會緩存 200 個堆棧幀復(fù)用對象,以獲得更好的執(zhí)行性能。整個程序跑下來,天知道要創(chuàng)建多少個這類對象。
用 functools.partial() 可以將函數(shù)包裝成更簡潔的版本。
>>> from functools import partial
>>> def test(a, b, c):
... print a, b, c
>>> f = partial(test, b = 2, c = 3) # 為后續(xù)參數(shù)提供命名默認值。
>>> f(1)
1 2 3
>>> f = partial(test, 1, c = 3) # 為前面的位置參數(shù)和后面的命名參數(shù)提供默認值。
>>> f(2)
1 2 3
partial 會按下面的規(guī)則合并參數(shù)。
def partial(func, *d_args, **d_kwargs):
def wrap(*args, **kwargs):
new_args = d_args + args # 合并位置參數(shù),partial 提供的默認值優(yōu)先。
new_kwargs = d_kwargs.copy() # 合并命名參數(shù),partial 提供的會被覆蓋。
new_kwargs.update(kwargs)
return func(*new_args, **new_kwargs)
return wrap
與函數(shù)相關(guān)內(nèi)容很多,涉及虛擬機底層實現(xiàn)。還要分清函數(shù)和對象方法的差別,后面會詳細說明。