Python 函數在底層長什么樣子?
楔子
函數是任何一門編程語言都具備的基本元素,它可以將多個動作組合起來,一個函數代表了一系列的動作。而且在調用函數時會干什么來著,沒錯,要創建棧幀,用于函數的執行。
那么下面就來看看函數在 C 中是如何實現的,生得一副什么模樣。
PyFunctionObject
Python 一切皆對象,函數也不例外,函數這種抽象機制在底層是通過 PyFunctionObject 結構體實現的。
// Include/cpython/funcobject.h
#define COMMON_FIELDS(PREFIX) \
PyObject *PREFIX ## globals; \
PyObject *PREFIX ## builtins; \
PyObject *PREFIX ## name; \
PyObject *PREFIX ## qualname; \
PyObject *PREFIX ## code; \
PyObject *PREFIX ## defaults; \
PyObject *PREFIX ## kwdefaults; \
PyObject *PREFIX ## closure;
typedef struct {
PyObject_HEAD
COMMON_FIELDS(func_)
PyObject *func_doc;
PyObject *func_dict;
PyObject *func_weakreflist;
PyObject *func_module;
PyObject *func_annotations;
PyObject *func_typeparams;
vectorcallfunc vectorcall;
uint32_t func_version;
} PyFunctionObject;
如果將宏展開的話,結構體就是下面這個樣子。
typedef struct {
PyObject_HEAD
PyObject *func_globals;
PyObject *func_builtins;
PyObject *func_name;
PyObject *func_qualname;
PyObject *func_code;
PyObject *func_defaults;
PyObject *func_kwdefaults;
PyObject *func_closure;
PyObject *func_doc;
PyObject *func_dict;
PyObject *func_weakreflist;
PyObject *func_module;
PyObject *func_annotations;
PyObject *func_typeparams;
vectorcallfunc vectorcall;
uint32_t func_version;
} PyFunctionObject;
我們來解釋一下這些字段,并實際獲取一下,看看它們在 Python 中是如何表現的。
func_globals:global 名字空間
def foo(a, b, c):
pass
name = "古明地覺"
print(foo.__globals__) # {..., 'name': '古明地覺'}
# 拿到的其實就是外部的 global名字空間
print(foo.__globals__ is globals()) # True
函數內部之所以可以訪問全局變量,就是因為它保存了全局名字空間。
func_builtins:builtin 名字空間
def foo(a, b, c):
pass
print(foo.__builtins__ is __builtins__.__dict__) # True
注意:在之前的版本中,函數內部是沒有這個字段的。
func_name:函數的名字
def foo(name, age):
pass
print(foo.__name__) # foo
當然不光是函數,方法、類、模塊都有自己的名字。
import numpy as np
print(np.__name__) # numpy
print(np.ndarray.__name__) # ndarray
print(np.array([1, 2, 3]).transpose.__name__) # transpose
除了 func_name 之外,函數還有一個 func_qualname 字段。
func_qualname:函數的全限定名
print(str.join.__name__) # join
print(str.join.__qualname__) # str.join
函數如果定義在類里面,那么它就叫類的成員函數,但它本質上依舊是個函數,和普通函數并無區別。只是在獲取全限定名的時候,會帶上類名。
func_code:函數對應的 PyCodeObject 對象
def foo(a, b, c):
pass
code = foo.__code__
print(code) # <code object foo at ......>
print(code.co_varnames) # ('a', 'b', 'c')
函數便是基于 PyCodeObject 構建的。
func_defaults:函數參數的默認值
def foo(name="古明地覺", age=16):
pass
# 打印的是默認值
print(foo.__defaults__) # ('古明地覺', 16)
def bar():
pass
# 沒有默認值的話,__defaults__ 為 None
print(bar.__defaults__) # None
注:默認值只會創建一次,所以默認值不應該是可變對象。
func_kwdefaults:只能通過關鍵字參數傳遞的 "參數" 和 "該參數的默認值" 組成的字典
def foo(name="古明地覺", age=16):
pass
# 打印為 None,這是因為雖然有默認值
# 但并不要求必須通過關鍵字參數的方式傳遞
print(foo.__kwdefaults__) # None
def bar(name="古明地覺", *, age=16):
pass
print(bar.__kwdefaults__) # {'age': 16}
加上一個 * 表示后面的參數必須通過關鍵字的方式傳遞。
func_closure:一個元組,包含了內層函數使用的外層作用域的變量,即 cell 變量。
def foo():
name = "古明地覺"
age = 17
def bar():
print(name, age)
return bar
# 內層函數 bar 使用了外層作用域中的 name、age 變量
print(foo().__closure__)
"""
(<cell at 0x000001FD1D3B02B0: int object at 0x7efe79d4a1c8>,
<cell at 0x000001FD1D42E310: str object at 0x7efe7921bc30>)
"""
print(foo().__closure__[0].cell_contents) # 17
print(foo().__closure__[1].cell_contents) # 古明地覺
注意:查看閉包屬性我們使用的是內層函數。
func_doc:函數的 docstring
def foo():
"""
hi,歡迎來到我的小屋
遇見你真好
"""
pass
print(foo.__doc__)
"""
hi,歡迎來到我的小屋
遇見你真好
"""
當我們在寫 Python 擴展的時候,由于編譯之后是一個 pyd,那么就會通過 docstring 來描述函數的相關信息。
func_dict:函數的屬性字典
def foo(name, age):
pass
print(foo.__dict__) # {}
函數在底層也是由一個類實例化得到的,所以它也可以有自己的屬性字典,只不過這個字典一般為空。
func_module:函數所在的模塊
import numpy as np
print(np.array.__module__) # numpy
除了函數,類、方法、協程也有 __module__ 屬性。
func_annotations:類型注解
def foo(name: str, age: int):
pass
# Python3.5 新增的語法,但只能用于函數參數
# 而在 3.6 的時候,聲明變量也可以使用這種方式
# 特別是當 IDE 無法得知返回值類型時,便可通過類型注解的方式告知 IDE
# 這樣就又能使用 IDE 的智能提示了
print(
foo.__annotations__
) # {'name': <class 'str'>, 'age': <class 'int'>}
像 FastAPI、Pydantic 等框架,都大量應用了類型注解。
func_typeparams:類型參數
from typing import TypeVar
T = TypeVar('T')
S = TypeVar('S')
def foo[T, S](x: T, y: S) -> list[S, T]:
return (y, x)
print(foo.__type_params__) # (T, S)
class A[T, S]:
def __init__(self, x: T, y: S):
self.x: T = x
self.y: S = y
a1 = A[int, float](3, 2.71)
a2 = A[str, dict]("hello", {})
print(A.__type_params__) # (T, S)
print(a1.__type_params__) # (T, S)
print(a2.__type_params__) # (T, S)
關于類型參數的更具體用法,可以查閱相關文檔,說實話如果是在 Python 里面,這種語法我估計一輩子都不會用。
vectorcallfunc vectorcall:矢量調用協議
函數本質上也是一個實例對象,在調用時會執行類型對象的 tp_call,對應 Python 里的 __call__。但 tp_call 屬于通用邏輯,而通用往往也意味著平庸,tp_call 在執行時需要創建臨時元組和臨時字典來存儲位置參數、關鍵字參數,這些臨時對象增加了內存分配和垃圾回收的開銷。
如果只是一般的實例對象倒也沒什么,但函數不同,它作為實例對象注定是要被調用的。所以底層對它進行了優化,引入了速度更快的 vectorcall,即矢量調用。
關于普通調用(tp_call)和矢量調用(vectorcall)的具體細節,后續會詳細說明。總之一個實例對象如果支持矢量調用,那么它也必須支持普通調用,并且兩者的結果是一致的,當對象不支持矢量調用時,會退化成普通調用。
uint32_t func_version:版本號,用于函數特化
函數特化是指根據函數的調用模式,生成更高效的特定版本代碼,特別是針對那些頻繁調用的函數。但函數特化有一個前提,就是函數本身不能夠發生改變,于是引入了 func_version 字段。
當函數的某些字段的值發生改變時,func_version 會重置為 0,而當底層看到 func_version 為 0 時,就知道函數發生改變了,特化失效。
def foo(x, y=10):
return x + y
# 以下操作會將 func_version 重置為 0
# 1. 修改默認參數
foo.__defaults__ = (20,)
# 2. 修改關鍵字默認參數
# 注:必須是指向一個新的字典,版本號才會重置
foo.__kwdefaults__ = {"z": 30}
# 3. 修改代碼對象
# (幾乎不可能直接修改,但可以通過某些高級技巧)
# 4. 修改注解
foo.__annotations__["return"] = int
# 5. 修改 vectorcall 函數指針
# (這是 C 級別的操作,Python 代碼通常無法直接觸及)
所以只要函數保持不變,Python 就會用特化版本來優化執行,而我們在工作中基本也不會修改上面這幾個字段。
小結
以上就是函數的底層結構,在 Python 里面是由 <class 'function'> 實例化得到的。
def foo(name, age):
pass
# <class 'function'> 就是 C 里面的 PyFunction_Type
print(foo.__class__) # <class 'function'>
但這個類底層沒有暴露給我們,我們不能直接用,因為函數通過 def 創建即可,不需要通過類型對象來創建。