“这语言设计的坑埋得真深”这个结论,值得商榷。从某种角度看,这不算设计失误,而是Python把“函数是一等对象”贯彻到底的必然结果。
def在Python里不是编译期的静态声明,而是一条可执行语句。解释器跑到def时,会创建一个function对象,把默认参数的求值结果直接绑定到这个对象的__defaults__属性上。这个过程只发生一次。如果每次调用都重新求值默认参数,那像 def connect(timeout=get_default_timeout()) 这种写法,get_default_timeout() 就会在每次调用时被触发,反而引入了不可预期的副作用和性能开销。
补充一个具体信息:你可以直接观察这个机制。定义 def f(a=[]): a.append(1); return a 之后,打印 f.__defaults__,会看到 ([],)。调用一次 f(),再打印 f.__defaults__,就变成了 ([1],)。数据就存在函数对象自己身上,没有幽灵,只有引用。
你提到的 a=None 判空重建是工程里的标准解法,不过这不是唯一的出路。如果业务逻辑确实需要跨次调用累积状态,利用这个特性反而能少写几行代码。比如实现一个简单的调用计数器或者缓存池,直接把状态挂在默认的可变参数上,连闭包或类都不用建。当然,这种做法可读性很差,团队协作时容易被当成bug修掉,所以一般不推荐。
另外,这种“默认参数共享状态”的现象并非Python独有。Ruby的默认参数也是定义时求值的;C++虽然机制不同,但如果默认参数传了引用或指针,同样会遇到类似的生命周期问题。核心在于区分“按值传递的不可变对象”和“按引用共享的可变对象”。
说到底,与其说是坑,不如说是语言把底层行为完全暴露给了开发者。有数据吗?其实去翻Python早期的邮件列表,Guido专门讨论过这个问题,结论就是保持def作为运行时语句的一致性,比给默认参数加特殊处理更符合整体哲学。
话说回来,刚学的时候谁没被这个 [1, 2] 突然变成 [1, 2, 1, 2] 搞过心态呢… 楼主那个前任的比喻挺精准的,记仇确实是它的核心特征 ( ̄▽ ̄)