楔子

函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作。而且在调用函数时会干什么来着,没错,要创建栈帧,用于函数的执行。

那么下面就来看看函数在 C 中是如何实现的,生得一副什么模样。

PyFunctionObject

Python 一切皆对象,函数也不例外,函数这种抽象机制在底层是通过 PyFunctionObject 结构体实现的。

// Include/funcobject.h

typedef struct {
    PyObject_HEAD
    PyObject *func_code;
    PyObject *func_globals;
    PyObject *func_defaults;
    PyObject *func_kwdefaults;
    PyObject *func_closure;
    PyObject *func_doc;
    PyObject *func_name;
    PyObject *func_dict;
    PyObject *func_weakreflist;
    PyObject *func_module;
    PyObject *func_annotations;
    PyObject *func_qualname;
    vectorcallfunc vectorcall;
} PyFunctionObject;

我们来解释一下这些字段,并实际获取一下,看看它们在 Python 中是如何表现的。

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_globals:global 名字空间

def foo(a, b, c):
    pass

name = "古明地觉"
print(foo.__globals__)  # {..., 'name': '古明地觉'}
# 拿到的其实就是外部的 global 名字空间
print(foo.__globals__ is globals())  # True

函数内部之所以可以访问全局变量,就是因为它保存了全局名字空间。

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 0x7f3f4398ac70: int object at 0x7f3f442413c0>, 
 <cell at 0x7f3f439e38b0: str object at 0x7f3f43b0ded0>)
"""

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_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 字段,表示全限定名。

print(str.join.__name__)  # join
print(str.join.__qualname__)  # str.join

函数如果定义在类里面,那么它就叫类的成员函数,当然它本质上依旧是个函数,和普通函数并无区别。只是在获取全限定名的时候,会带上类名。

func_dict:函数的属性字典

def foo(name, age):
    pass

print(foo.__dict__)  # {}

函数在底层也是由一个类实例化得到的,所以它也可以有自己的属性字典,只不过这个字典一般为空。

func_weakreflist:弱引用列表

这里不做讨论。

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 等框架,都大量应用了类型注解。

vectorcall:矢量调用协议

函数本质上也是一个实例对象,在调用时会执行类型对象的 tp_call,对应 Python 里的 __call__。但 tp_call 属于通用逻辑,而通用往往也意味着平庸,tp_call 在执行时需要创建临时元组和临时字典来存储位置参数、关键字参数,这些临时对象增加了内存分配和垃圾回收的开销。

如果只是一般的实例对象倒也没什么,但函数不同,它作为实例对象注定是要被调用的。所以底层对它进行了优化,引入了速度更快的 vectorcall,即矢量调用。而一个实例对象如果支持矢量调用,那么它也必须支持普通调用,并且两者的结果是一致的,如果对象不支持矢量调用,那么会退化成普通调用。

小结

以上就是函数的底层结构,在 Python 里面是由 <class 'function'> 实例化得到的。

def foo(name, age):
    pass

# <class 'function'> 就是 C 里面的 PyFunction_Type
print(foo.__class__)  # <class 'function'>

但这个类底层没有暴露给我们,所以不能直接用,因为函数通过 def 创建即可,不需要通过类型对象来创建。

后续会介绍更多关于函数相关的知识。


 

欢迎大家关注我的公众号:古明地觉的编程教室。

如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。