在 Python 教程里, 针对默认参数, 给了一个 "重要警告" 的例子:
- def f(a, L=[]):
- L.append(a)
- return L
- print(f(1))
- print(f(2))
- print(f(3))
默认值只会执行一次, 也没说原因. 会打印出结果:
- [1]
- [1, 2]
- [1, 2, 3]
因为学的第一门语言是 Ruby, 所以感觉有些奇怪. 但肯定的是方法 f 一定储存了变量 L.
准备知识: 指针
p 指向不可变对象, 比如数字. 则相当于 p 指针指向了不同的内存地址.
p 指向的是可变对象, 比如 list.list 自身的改变, 并不会改变 list 对象自身所在的内存地址. 所以 p 指向的内存地址不变.
- >>> p = 1
- >>> id(p)
- 4523801232
- >>> p = p + 1
- >>> id(p)
- 4523801264
- >>> p = 11
- >>> id(p)
- 4523801552
- >>> p = []
- >>> id(p)
- 4527080640
- >>> p.append(11)
- >>> id(p)
- 4527080640
根本原因
Python 函数的参数默认值, 是在编译阶段就绑定了.(写代码时就定义了.)
下面是一段从 Python Common Gotchas 中摘录的原因解释:
Python's default arguments are evaluated once when the function is defined, not each time the function is called(like it is in say, Ruby). This means that if you use a mutable default argument and mutate it, you will and have mutated that object for all future calls to the function as well.
由此可知:
在运行代码时, 运行到函数定义时, 默认参数的表达式就被执行了.
函数调用时, 不会再次运行默认参数的表达式. 这点和 Ruby 完全不同.
由此可知, 如果默认参数, 指向一个不变对象, 例如 L = 1. 那么在函数调用时, 在函数体内对 L 重新赋值, L 其实是一个新的指针, 指向的是一个新的内存地址. 而原来默认参数 L 本身及指向的内存地址, 已经储存在最开始编译时的函数定义中. 可以用__default__查看.
如果默认参数指向的是一个可变对象, 如 list, 那么 L.append(a) 是对可变对象自身的修改, L 指向的内存地址不变. 所以每次调用函数, 默认参数取出的都是这个内存地址的对象.
第三条, 修改上面的例子:
- def f(a, L = 1):
- L = a
- print(id(L))
- return L
- print("self",id(f.__defaults__[0]))
- print(f(1))
- print("self",id(f.__defaults__[0]))
- print(f(33))
- print("self",id(f.__defaults__[0]))
- # 运行结果:
- self 4353170064
- 4353170064
- 1
- self 4353170064
- 4353171088
- 33
- self 4353170064
默认参数 L, 在编译阶段就绑定了, 储存在__default__内. 函数体内的 L = a 表达式, 生成的是新的变量. 返回的 L 是新的变量, 和默认参数无关.
第四条, 还是上面的例子, 改一下默认参数的类型为可变对象 list:
- def f(a, L = []):
- L.append(a)
- print(id(L))
- return L
- # L = f(1)
- print("self",id(f.__defaults__[0]))
- print(f(1))
- print("self",id(f.__defaults__[0]))
- print(f(33))
- print("self",id(f.__defaults__[0]))
- # 返回结果
- self 4337586048
- 4337586048
- [1]
- self 4337586048
- 4337586048
- [1, 33]
- self 4337586048
由 id 号可知, 返回的是默认参数自身.
如何避免这个陷阱带来不必要麻烦
- def f(a, L = None):
- if L is None:
- L = []
- L.append(a)
- return L
为什么 Python 要这么设计
Stack Overflow 上争论很多.
Ruby 之所以没有这个问题, 我想是因为 Ruby 的 def 关键字定义了一个封闭作用域, 任何数据都必须通过参数传入到方法内, 才能用.
和 Ruby 比, Python 参数的作用被大大消弱了. Python 的出现晚于 Ruby, 其创始人肯定参考了 Ruby 的设计. 抛弃了这个设计, 选择了类似 JavaScript 的函数方式. def 定义的函数, 执行时是可以接收外部作用域的变量的.
有观点认为:
出于 Python 编译器的实现方式考虑, 函数是一个内部一级对象. 而参数默认值是这个对象的属性. 在其他任何语言中, 对象属性都是在对象创建时做绑定的. 因此, 函数参数默认值在编译时绑定也就不足为奇了.
来源: https://www.cnblogs.com/chentianwei/p/11963383.html