【Python】python中的函数

我们先看一个引例:

python
class A:
    pass


def f():
    print(1)


a = A()
b = f()

这段 代码中我们声明了一个类A,声明了一个函数f,通过A()实例化一个对象,f()就调用函数。这里不禁思考,程序在执行A(),f()之前 上面代码声明类A与声明函数f已经存放在内存中去了,当A()实例化对象时我们在类与对象一文中讲过,python解释器会调用type元类__call__方法,那么f()函数调用又会干嘛?对于解释器来说,都是一个内存地址,加了一个括号,我们在声明类A与函数f时完全可以把这两个名对调,即class f,def A()。Python解释器是机器,无法从"名称()"区分到底是创建类实例还是调用方法,我们已知的是"类()"就会调用创建它的类的__call__方法,那意味着函数f也有__call__方法,如果没__call__方法,不然解释器碰到f(),它也不清楚是类还是函数,反正一个"名称()"就去调创建它的类的__call__方法,从这个角度对讲,f也是一个类,创建它的类也有__call__方法,而且创建A的类与创建f的类不同,假设相同,那就会调用同一个__call__方法,那就没区别了。我们打印一下看看f是什么:

python
class A:
    pass


def f():
    print(1)


print(type(A))
print(type(f))

运行结果

text
<class 'type'>
<class 'function'>

结果表明A是type类,f是function类,两者都是类,我们调用方法是这样的f(),那是不是意味着function类有__call__方法,而这个类就是types.FunctionType

示例:

python
import types

# 1. 准备一张图纸
base_code = (lambda: None).__code__

# 2. 正常调用模板类创建实例
f = types.FunctionType(base_code, {})

# 3. 打印这个实例的类型
print(type(f))

print(dir(types.FunctionType))

运行结果

text
<class 'function'>
['__annotations__', '__builtins__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__getstate__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__type_params__']

FunctionType类确实有__call__方法。所以f()就是调用FunctionType类的__fall__方法。f是FunctionType类的实例。

示例:

python
from types import FunctionType

print("__call__" in FunctionType.__dict__)

运行结果

python
True

既然f也是一个类,那这个类有哪些方法呢

text
def f():
    pass

print(f.__dict__)
print(dir(f))

运行结果

python
{}
['__annotations__', '__builtins__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__getstate__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__type_params__']

本身没有任何属性与方法,但是可以调用很多方法,一方面是来自默认继承object类的方法,还有一些是来自object方法,要么继承要么创建它的类有有这些方法,毕竟f本身是FunctionType类的实例。没看到显式继承,那就是其他方法是FunctionType类的方法。

python
from types import FunctionType


def f():
    pass


print(f.__dict__)
print(dir(f))

print(FunctionType.__dict__)

obj_keys = object.__dict__.keys()

# 从f可调用的方法中,排除来自object的方法后是否都存在FunctionType拥有的方法中
for key in dir(f):
    if key in obj_keys:
        continue
    if key not in FunctionType.__dict__:
        print(key, "FunctionType不在类中")

运行结果

text
{}
['__annotations__', '__builtins__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__getstate__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__type_params__']
{'__new__': <built-in method __new__ of type object at 0x104aa2630>, '__repr__': <slot wrapper '__repr__' of 'function' objects>, '__call__': <slot wrapper '__call__' of 'function' objects>, '__get__': <slot wrapper '__get__' of 'function' objects>, '__closure__': <member '__closure__' of 'function' objects>, '__doc__': <member '__doc__' of 'function' objects>, '__globals__': <member '__globals__' of 'function' objects>, '__module__': <member '__module__' of 'function' objects>, '__builtins__': <member '__builtins__' of 'function' objects>, '__code__': <attribute '__code__' of 'function' objects>, '__defaults__': <attribute '__defaults__' of 'function' objects>, '__kwdefaults__': <attribute '__kwdefaults__' of 'function' objects>, '__annotations__': <attribute '__annotations__' of 'function' objects>, '__dict__': <attribute '__dict__' of 'function' objects>, '__name__': <attribute '__name__' of 'function' objects>, '__qualname__': <attribute '__qualname__' of 'function' objects>, '__type_params__': <attribute '__type_params__' of 'function' objects>}

结果就是:f可调用的方法中,排除来自object的方法后都在FunctionType拥有的方法中。

独有的方法:

python
from types import FunctionType


def f():
    pass


obj_keys = object.__dict__.keys()
f_list = []

# 从f可调用的方法中,排除来自object的方法后是否都存在FunctionType拥有的方法中
for key in dir(f):
    if key in obj_keys:
        continue
    if key in FunctionType.__dict__:
        f_list.append(key)

print(f_list)

运行结果

text
['__annotations__', '__builtins__', '__call__', '__closure__', '__code__', '__defaults__', '__dict__', '__get__', '__globals__', '__kwdefaults__', '__module__', '__name__', '__qualname__', '__type_params__']

独有方法说明:

  • annotations 存放类型提示(Type Hints)的字典。如果你写了 def f(x: int) -> str:,这里面就会记录 {'x': <class 'int'>, 'return': <class 'str'>}。
  • builtins 存放 Python 内置大招的仓库。一个字典,里面存放着 Python 自带的、不需要任何 import 就能直接用的内置函数(如 len(), str(), int(), Exception 等)。
  • call 触发函数执行的开关。当你写下 f() 时,解释器会瞬间去调用 FunctionType.call。如果是通过实例 f.call 访问,它会动态生成一个 method-wrapper 临时零件。
  • closure 实现“闭包(Closure)”的内存保鲜盒。如果你的函数里嵌套了另一个函数,内部函数引用了外部函数的局部变量,即使外部函数执行完了,这个局部变量也会被打包存在内部函数的 closure 里(里面是一堆 cell 对象),这就是闭包不丢数据的底层秘密。
  • code 函数的静态设计图纸。指向 CodeType 字节码对象(里面藏着所有的 co_ 属性和冷冰冰的 0-1 指令)。
  • defaults 存放“位置参数”默认值的元组(Tuple)。如果你写了 def f(a=1, b=2):,这里面就会存下 (1, 2)。
  • kwdefaults 存放“只能用关键字传参(Keyword-only)”默认值的字典(Dict)。如果你写了 def f(*, name='Peter'):,这里面就会存下 {'name': 'Peter'}。
  • module 记录该函数是在哪个 Python 文件(模块)里被定义出来的。如果在当前文件运行,它通常是 "main";如果在 Django 的视图文件里,它可能是 "myapp.views"
  • dict 函数的私人账本。:默认是空的。你可以像对待普通对象一样,强行给函数塞入自定义数据(如 f.status = "success"),这些数据都会存进 dict
  • get 实现“方法绑定(Bound Method)”的加工厂。Python 里的普通函数,是怎么变成类里的“方法”的?就是靠它!当你在类里写了 def walk(self): pass,然后用实例调用 obj.walk() 时,底层就是通过 get 动态把 obj 实例和函数捆绑在一起,变成了带 self 的方法对象。
  • globals 函数能看见的“全局世界(命名空间)”。这是一个只读字典。函数体内部如果用了 print() 或者调用了你在外面 import 的模块,它唯一的地图就是去这个 globals 字典里查找。
  • name 函数的本名(字符串)。比如 def hello(): pass,它的 name 就是 "hello"。
  • qualname 函数的全路径限定名(Qualified Name)。如果你的函数是嵌套在某个类或者另一个函数里的(比如 class A: def hello(self): pass),普通名 name 只是 "hello",而 qualname 会吐出全路径 "A.hello",方便报错时精准定位。
  • type_params 新版 Python 3.12+ 专有的泛型参数记录器。当你使用新语法写了泛型函数(如 def f[T](x: T): pass)时,这里面会用元组记录这个泛型 T 的约束和定义。

当我们在定义一个如函数如 "def f(): pass",解释器读完这段定义后把它存放在内存区中,通过f名指向,当执行f()就会去调用FunctionType类的__call__方法。而FunctionType类是通用的(当你尝试去继承时会提示你不能拥有子类),那意味着这个__call__方法也是每个函数共用的,那既然这样,由于我们定义的函数是不一样的,____call____方法里得有相同名称的一个入口去执行真正的函数定义。这个入口就是__code__属性。

我们来看看__code__里面会有什么

python
from types import FunctionType


def f():
    pass


obj_keys = object.__dict__.keys()
code_list = []

# 从f可调用的方法中,排除来自object的方法后是否都存在FunctionType拥有的方法中
for key in dir(f.__code__):
    if key in obj_keys:
        continue
    else:
        code_list.append(key)

print(code_list)

运行结果

text
['_co_code_adaptive', '_varname_from_oparg', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_exceptiontable', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lines', 'co_linetable', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_positions', 'co_posonlyargcount', 'co_qualname', 'co_stacksize', 'co_varnames', 'replace']

  • _co_code_adaptive 新版 Python 3.11+ 专有,自适应(Adaptive)优化后的特化字节码。这是 Python 3.11 引入的“黑科技”。当一个函数被高频调用(比如跑了一万次)时,Python 的虚拟机会嫌普通的 co_code 太慢,会自动把里面的一些通用指令偷偷升级替换成专为特定类型加速的“快捷指令”。这个属性存的就是优化后的字节码副本。

  • _varname_from_oparg 内部自省工具方法。 它是一个内部辅助函数,用来将底层的字节码参数(操作数数字)反向翻译成人类能看懂的变量名。

  • co_argcount 普通位置参数的总个数。

  • co_code 真正的二进制字节码(Bytecode)命令序列。

  • co_varnames 局部变量名和位置参数名的元组。比如你写了 def f(a): x = 1,这里面就会老老实实记录 ('a', 'x')。

  • co_names 函数内部用到的「外部名字」或「全局名字」。如果你在函数内部写了 print(math.pi),因为 print、math 和 pi 都不是函数内部自己定义的,它们会作为外部名字被收录在 co_names 里。

  • co_nlocals 局部变量的总个数(整数)。就是 len(co_varnames) 的值。C 语言大循环在给函数创建执行格子(栈帧)时,会读取这个数字,瞬间在内存里一次性劈出对应数量的空间。

  • co_cellvars 与 co_freevars 闭包(Closure)专用的作用域纽带。如果你的函数里嵌套了别的函数,co_cellvars 记录“被内部函数偷窥的本层变量名”;co_freevars 记录“本层函数去偷窥外层函数的变量名”。它们是闭包不丢数据的幕后桥梁。

  • co_posonlyargcount 只能用位置传参的参数个数(斜杠 / 左边的参数)。如果你定义了 def f(a, /, b):,那么 co_posonlyargcount 就是 1。

  • co_kwonlyargcount 只能用关键字传参的参数个数(星号 * 右边的参数)。如果你定义了 def f(a, *, name):,那么 co_kwonlyargcount 就是 1。

  • co_stacksize 该函数运行时所需的虚拟机栈的最大深度(整数)。Python 编译器极其精明,在读完你的代码定义后,通过计算数学模型,就已经100%预知了这个函数在运算时,内存栈里最多会同时堆叠几个数据。它把这个最大值写在 co_stacksize 里,C 语言底层看到后,就会精准分配内存,绝不多浪费一个字节。

  • co_filename 定义该函数的 Python 文件的物理绝对路径(字符串)。记录了你的代码写在哪个文件里,比如 '/Users/xxxxx/code/demo.py'。

  • co_name 与 co_qualname 函数的本名与全路径限定名,报错堆栈里显示的 in my_func 的名字,就是从这里抓的。

  • co_firstlineno 该函数在源代码文件里的“起始行号”(整数)。如果你在第 50 行写了 def my_func():,这个值就是 50。

  • co_lines、co_linetable、co_lnotab、co_positions 由于 co_code 里的二进制指令是一长串连续的数字,报错时,Python 是怎么知道到底是哪一行代码崩了呢?就是靠这几个属性。它们是一张复杂的对照表,记录着“第 0-4 字节的二进制命令对应源码第 51 行第 3 列,第 5-8 字节对应源码第 52 行”。有了这张映射表,哪怕代码编译成了机器字节码,报错时依然能精准定位到你写下的那一行文本。

  • co_consts 函数内部所有写死的“静态常量池”(元组)。只要你在函数里写了字面量(比如数字 100、字符串 'Hello'),通通都会被编译期直接打包收录在这里。每个函数的 co_consts 默认第一个元素都是 None(作为默认返回值)。

  • co_flags 函数的“特性标志位”(整数/位掩码)。一个数字。通过二进制位运算代表函数的特殊身份:这个函数是不是一个生成器(Generator)?是不是一个异步协程(Async def)?是不是接收 *args?是不是接收 **kwargs?唯一的 call 进来后,通过读取这个数字标志,就能瞬间决定用哪种模式去驱动它。

  • co_exceptiontable 异常处理(try-except)映射表。如果你在函数里写了 try...except,编译器会把异常捕获的范围和跳转目标做成一张表格存在这里。一旦大循环报错,立刻来这里查表,决定是跳到 except 块还是直接崩溃。

  • replace 一个用于动态克隆并修改图纸的方法。因为 code 里的所有 co_ 属性在 C 底层都是只读写死的,你不能执行 f.code.co_name = 'new_name'。但 Python 官方提供了一个官方合法的后门——f.code.replace(co_name='new_name'),它会强行在内存里克隆一张新图纸,并把改好的新图纸发回给你。

通过FunctionType创建函数

python
def __new__(cls,
            code: CodeType,
            globals: dict[str, Any],
            name: str | None = ...,
            argdefs: tuple[object, ...] | None = ...,
            closure: tuple[_Cell, ...] | None = ...) -> Self
The base class of the class hierarchy.
When called, it accepts no arguments and returns a new featureless instance that has no instance attributes and cannot be given any.

FunctionType类的__new__方法需要传递的参数

  • code: CodeType(必填项) 对应函数属性:f.code,函数的静态设计图纸

  • globals: dict[str, Any](必填) 对应函数属性:f.globals,函数的外部运行环境,这是一个字典。函数在运行图纸指令时,如果遇到了非局部变量(比如调用了外部的 print,或者你 import 的模块),它唯一的寻宝图就是这个字典。

  • name: str | None(可选) 对应函数属性:f.name,函数的身份证本名(字符串)

  • argdefs: tuple[object, ...] | None(可选) 对应函数属性:f.defaults,位置参数的默认值元组。如果你希望新函数出生就自带默

  • closure: tuple[_Cell, ...] | None(可选) 对应函数属性:f.closure,闭包(Closure)的内存保鲜盒(包含一组底层的 Cell 对象)。如果这个图纸(code)内部引用了外层函数的局部变量(属于嵌套函数),你就必须通过这个参数,把外层环境的变量格子(Cell)强行捆绑在新函数身上。这样,即使外层函数死掉了,新函数依然能通过这个保鲜盒随时读取外层的数据。 示例:

python
from types import FunctionType

# 🚀 一行代码:直接组装出一个名字叫 "hacked_f",自带默认值 10,能读取全局变量 SECRET 的活函数
f = FunctionType((lambda x, y: x + y + SECRET).__code__, {"SECRET": 100, "__builtins__": __builtins__}, "hacked_f", (10,))

# 精准测试运行
print(f.__name__)      # 输出: hacked_f
print(f.__defaults__)  # 输出: (10,)
print(f(5))            # 运算: 5 (x) + 10 (默认值y) + 100 (全局SECRET) = 115

运行结果

text
hacked_f
(10,)
115

这里其实本质就是通过一个函数模版,创造出一个函数类。假设我们很多功能,函数实现几乎差不多,是不是可以复用,外部传不同的参数,反正所有函数都是FunctionType创建的,有些属性或方法函数自身也拿得到,这就节省内存了。

下一篇 没有了