python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python 可变默认参数 和 闭包延迟绑定

Python 可变默认参数和闭包延迟绑定的实现

作者:Csvn

本文主要介绍了Python可变默认参数和闭包延迟绑定的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

1. 知识点简介

Python 的「灵活」有时也是双刃剑。下面这两个坑点隔三差五就出现在生产事故和面试题中,而且它们都来自同一个根源:Python 的绑定时机问题

理解这两个坑 = 理解 Python 作用域 + 绑定机制的关键。

2. 坑 ①:可变默认参数

现象

def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item(1))          # [1]
print(add_item(2))          # [1, 2]  👈 预期是 [2]!
print(add_item(3))          # [1, 2, 3]

每次调用 add_item 时,items 指向的都是 同一个列表对象

原因

Python 的默认参数值在函数定义时def 语句执行时)计算并绑定。之后每次调用若不传该参数,用的都是同一个对象。

验证一下:

def test(items=[]):
    print(id(items))  # 每次调用都打印相同的内存地址

test()  # 4395790912
test()  # 4395790912
test()  # 4395790912

正确姿势

# ✅ 用 None 做哨兵,函数内部创建新对象
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item(1))   # [1]
print(add_item(2))   # [2]  ✅ 符合预期

经典变种

# 不只是 list,任何可变对象都一样
def bad(d={}):
    d["count"] = d.get("count", 0) + 1
    return d

print(bad())  # {'count': 1}
print(bad())  # {'count': 2}  ❌
# 甚至连 Datetime 和 lambda 也不例外
import datetime

def log_time(t=datetime.datetime.now()):
    print(t)

log_time()  # 每次打印相同的时刻 ❌

3. 坑 ②:闭包延迟绑定

现象

def create_multipliers():
    return [lambda x: i * x for i in range(5)]

multipliers = create_multipliers()
for m in multipliers:
    print(m(2))  # 输出:8 8 8 8 8  👈 预期:0 2 4 6 8

所有函数都返回 8?因为循环结束时 i = 4,所有闭包引用的都是同一个变量 i最终值

原理拆解

# 等价写法,更容易看穿
funcs = []
for i in range(5):
    funcs.append(lambda x: i * x)

i = 4  # 循环结束后的 i

for f in funcs:
    print(f(2))  # 全部 8

lambda 捕获的是变量 i 本身(引用),而非创建时 i 的值。当闭包真正执行时,才去查找 i 的当前值。

正确姿势

方式 1:默认参数绑定(利用坑 ① 的特性反制)

def create_multipliers():
    return [lambda x, i=i: i * x for i in range(5)]
    #          ^^^^  默认参数在循环每次迭代时求值,i 的值被固定

方式 2:functools.partial

from functools import partial

def multiply(x, i):
    return i * x

def create_multipliers():
    return [partial(multiply, i) for i in range(5)]

方式 3:闭包嵌套(立即执行外层函数)

def create_multipliers():
    def make_multiplier(i):
        return lambda x: i * x
    return [make_multiplier(i) for i in range(5)]

三种方式都正确输出:0 2 4 6 8

4. 组合起来:一个更隐蔽的例子

def create_actions():
    actions = []
    for i in range(3):
        def action(item, cache=[]):  # 默认参数绑定 + 可变对象
            cache.append(item)
            return f"i={i}, cache={cache}"  # i 是延迟绑定的
        actions.append(action)
    return actions

actions = create_actions()
print(actions[0]("a"))  # i=2, cache=['a']  ❌ i 预期 0
print(actions[1]("b"))  # i=2, cache=['a', 'b']  ❌ 双重坑
print(actions[2]("c"))  # i=2, cache=['a', 'b', 'c']

同时踩了两个坑。修正后:

def create_actions():
    actions = []
    for i in range(3):
        def action(item, cache=None):
            if cache is None:
                cache = []
            cache.append(item)
            return f"i={i}, cache={cache}"
        actions.append(action)
    return actions

嗯…这样也只修了可变参数坑,i 还是延迟绑定。需要两个一起修:

def create_actions():
    actions = []
    for i in range(3):
        def action(item, cache=None, i=i):  # 同时解决两个坑
            if cache is None:
                cache = []
            cache.append(item)
            return f"i={i}, cache={cache}"
        actions.append(action)
    return actions

5. 避坑清单

# ❌ 不要这样写
def process(data=[]):           # 可变默认参数
    ...

# ✅ 改成这样
def process(data=None):
    data = data or []           # 或严格判断:if data is None: data = []


# ❌ 不要在循环中直接创建闭包
funcs = [lambda: i for i in range(5)]

# ✅ 绑定当前值
funcs = [lambda i=i: i for i in range(5)]

6. 底层原理速记

概念理解要点
函数定义时def 语句执行时,默认参数对象被创建并绑定到函数对象上
函数调用时未传参 → 复用绑定好的默认对象;传参 → 使用新对象
闭包(closure)内层函数引用了外层函数的变量,形成闭包,变量本身被「捕获」而非值
LEGB 规则闭包执行时按 Local → Enclosing → Global → Built-in 查找变量

简单记忆口诀:默认参数看定义时,闭包变量看执行时

7. 总结

这两个坑是 Python 开发者的「成人礼」。踩过,修过,才算是真正理解了 Python 的对象模型和作用域。

到此这篇关于Python 可变默认参数和闭包延迟绑定的实现的文章就介绍到这了,更多相关Python 可变默认参数和闭包延迟绑定内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文