Python函数-函数进阶
2025-8-31
| 2025-8-31
Words 2548Read Time 7 min
type
status
date
slug
summary
tags
category
icon
password

📝 函数参数传递

💡
在 CPython 中,所有参数传递都是“对象指针(内存地址)的按值拷贝”——官方称为 call-by-object-reference。

1、内存模型总览

ax 各自保存 同一份对象 地址 1001。
对象本身 的修改,取决于 1001 指向的对象 是否可变

2、不可变类型:重新赋值 = 新建对象

字节码关键指令

形参 n 指向 新对象,外部 a 仍指向旧对象 → 值不变。

3、可变类型:原地修改 = 外部可见

1001 指向 可变列表对象,原地改内部数组 → 外部同步看到。

4、重新赋值 vs 原地修改对照表

场景
代码
结果
原因
不可变 + 重新赋值
x = x + 1
外部不变
新建对象
可变 + 原地修改
x.append(1)
外部变化
同一对象
可变 + 重新赋值
x = [99]
外部不变
形参指向新对象

5、默认参数为可变类型

在 Python 中,默认参数只在函数定义时创建一次,并把可变对象(如列表、字典、集合)的引用保存下来。
  • 无论是否显式传递该参数,只要使用默认参数,就始终共享同一份对象;
  • 该对象的 内存地址(id)永不改变;
  • 在函数体内原地修改默认对象,会影响后续调用;
  • 显式传参会替换默认引用,此时 id 与值按传入对象决定。

5.1、内存图:默认对象只创建一次

地址 0x1000 在函数 整个生命周期内不变。
每次调用只是把 地址 0x1000 塞进形参变量。

5.2、代码实验:id 不变,值递增

5.3、字节码证明:默认值只计算一次

后续每次 LOAD_DEREF 都取出 同一地址

5.4、修复方案:用 None 做哨兵

此时省略默认参数时 id 每次不同,不再累积。

📝 函数返回值传递

在 CPython 中,“函数返回值” 本质上只干一件事:把 对象在堆上的内存地址(PyObject)复制给调用者。
场景
修改/重新赋值
调用者是否可见
原因
返回 不可变对象
重新绑定
新对象,地址已变
返回 可变对象
原地修改
同一地址,内容变
返回 可变对象
重新绑定
变量指向新地址

1、底层机制:返回值 = 地址按值拷贝

return obj对象 1001 的地址 复制给调用者变量 r
无论对象可变或不可变,拷贝的都是 地址值本身

2、不可变类型:重新绑定 = 新建对象

字节码关键指令

原对象 10 不变;调用者拿到 新对象 11 的地址。

3、可变类型:原地修改 = 外部可见

地址始终为 1001;内容被修改 → 调用者 同步看到。

4、重新绑定 vs 原地修改对照

操作
代码
结果
解释
重新绑定
lst = [99]
外部不变
新地址,原地址弃用
原地修改
lst.append(9)
外部变化
同一地址,内容变

📝 函数做容器元素

💡
在 Python 里,“函数即对象”,因此任何函数都可以被
  1. 塞进 列表、元组、字典、集合
  1. 作为 key 或 value
  1. 做 回调、策略、插件、装饰器链

1、可哈希性:函数能不能当 dict、set 的 key?

普通函数 默认可哈希(\\hash\\ 继承自 object)。
只要 函数名相同且内容相同(同一地址),就满足 hash(f) == hash(f)。
若自定义类实例函数(如 lambda 捕获外部变量)需注意可变闭包。

2、容器用法全景表

容器
写法
场景
列表
[add, sub]
回调链 / 策略列表
元组
(add, mul)
不可变策略表
字典
{add: "加法", mul: "乘法"}
函数映射表
集合
{add, mul}
去重函数集合
嵌套
[{"op": add, "name": "+"}]
配置化插件

3、实战代码:函数列表动态调度

4、函数映射表(函数做 key)

5、函数嵌套容器:装饰器链

6、易错点 & 提示

6.1、lambda 可变捕获:

6.2、函数名冲突:同名函数会覆盖旧地址,导致容器里引用失效。

📝 函数名赋值

💡
在 Python 里,“函数名”本质上只是一个指向函数对象的变量,因此它服从普通变量的全部规则:
  • 可以赋值给其它变量(别名)
  • 可以重新绑定到新对象(覆盖原函数)
  • 原函数对象不受影响(除非无人引用才会被 GC)

1、函数名即变量:底层模型

执行时,创建一个函数对象(地址 0x1001)。
名字 foo 绑定到该地址:foo -> 0x1001。
类型检查:type(foo) → <class 'function'>。

2、把函数名赋值给其它变量(别名)

两个名字共享同一对象,调用成本为 零拷贝。
常用于 回调注册、策略表、装饰器链。

3、给函数名重新赋值(覆盖)

原函数对象 0x1001 仍存在;只是名字 foo 不再指向它。
若无人引用旧对象,GC 会回收。

4、典型用途

场景
代码
说明
策略表
ops = [add, sub, mul]
函数列表
装饰器链
f = cache(f)
层层包装
插件热插拔
process = plugin_map[plugin_name]
运行时切换
别名回退
old_sqrt = math.sqrt; math.sqrt = my_sqrt
monkey-patch

5、易错点

5.1、lambda 闭包陷阱

修复:lambda x, i=i: x + i

5.2、循环引用导致内存泄漏

容器里存函数,函数又捕获容器 → 需要 weakref 解决。

📝 作用域

1、作用域层级速记表(LEGB)

名称
英文
触发场景
只读/可写
Local
局部
当前函数内部
默认可写
Enclosing
闭包
外层嵌套函数
只读,需 nonlocal 改写
Global
全局
模块顶层
只读,需 global 改写
Built-in
内建
Python 内置
只读

2、函数是最小作用域边界

函数定义 会创建一个 新的局部命名空间。
函数执行 时,所有在函数体内 赋值 的标识符默认落入 Local。
函数返回 后,局部命名空间被销毁(闭包除外)。

3、全局变量 vs 局部变量

特征
全局变量
局部变量
定义位置
模块顶层
函数体内
生命周期
解释器启动-关闭
函数调用-返回
访问规则
任何地方可读
仅函数体内可读/写
修改规则
函数内 只读(除非 global
默认可写

4、代码实验:读/写差异

5、nonlocal:修改外层函数局部

nonlocal cntinnercnt 视为 Enclosing 作用域变量。

6、全局变量与循环引用陷阱

全局变量长期存在,可能被多个模块共享;过度使用会导致 命名冲突 与 测试困难。
用 函数参数 或 类属性 替代全局。
必须用全局时,放在 专用模块(如 config.py)。
  • 开发
  • Python
  • Python函数-函数基础Python函数-函数高级
    Loading...