type
status
date
slug
summary
tags
category
icon
password
📝 函数参数传递
在 CPython 中,所有参数传递都是“对象指针(内存地址)的按值拷贝”——官方称为 call-by-object-reference。
1、内存模型总览
a
和 x
各自保存 同一份对象 地址 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 里,“函数即对象”,因此任何函数都可以被
- 塞进 列表、元组、字典、集合
- 作为 key 或 value
- 做 回调、策略、插件、装饰器链
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 cnt
让inner
把cnt
视为 Enclosing 作用域变量。6、全局变量与循环引用陷阱
全局变量长期存在,可能被多个模块共享;过度使用会导致 命名冲突 与 测试困难。
用 函数参数 或 类属性 替代全局。
必须用全局时,放在 专用模块(如 config.py)。