创建: 2022-09-01 10:44
更新: 2022-09-02 11:41
http://scz.617.cn:8/python/202209011044.txt
接上篇,《讨论Python函数默认参数的坑》
Digg的程序员应该没有做恰当的单元测试。
只看作用域的话,f()默认形参L更像是C语言函数中的静态局部变量。
网上有很多文章讲这个坑,但不刻意搜的话,并不"常见"。
Python Mutable Defaults Are The Source of All Evil - [2018-08-14]
https://florimond.dev/en/posts/2018/08/python-mutable-defaults-are-the-source-of-all-evil/
文中提到
Do not use mutable default arguments in Python. In Python, when passing a mutable value as a default argument in a function, the default argument is mutated anytime that value is mutated. Here, "mutable value" refers to anything such as a list, a dictionnary or even a class instance. The solution is simple, use None as a default and assign the mutable value inside the function.
作者意思是,这样改写
def f_2 ( L=None ) :
if L is None :
L = []
L.append( 1 )
print( L )
print( hex( id( L ) ) )f_2()
f_2()
f_2()
print( hex( id( [0] ) ) )
f_2([0])
我平时写代码就这么写的,但确实不知道前面那个坑,只是简单地喜欢用None、True这类非可变对象做默认参数。
上述代码依次输出
[1]
0xb765ef48
[1]
0xb765ef48
[1]
0xb765ef48
0xb765ef48
[0, 1]
0xb765ef48
5次地址均相同,应该是回收再分配所致。
网友「轩辕御龙」提到,Python函数实际也是对象,函数默认参数会保存在它的"__defaults__"字段里,所以在整个程序生命周期里函数默认参数都没有回收,大概是这样?
我没细究过,简单测了一下,他这个说法可能是对的。
def f_3 ( L=[] ) :
L.append( 1 )
print( L )
print( f_3.__defaults__[0] )
print( f"&f_3={id(f_3):#x} &f_3.__defaults__[0]={id(f_3.__defaults__[0]):#x} &L={id(L):#x}" )f_3()
f_3()
f_3()
f_3([0])
上述代码依次输出
[1]
[1]
&f_3=0xb7659b68 &f_3.__defaults__[0]=0xb765ef88 &L=0xb765ef88
[1, 1]
[1, 1]
&f_3=0xb7659b68 &f_3.__defaults__[0]=0xb765ef88 &L=0xb765ef88
[1, 1, 1]
[1, 1, 1]
&f_3=0xb7659b68 &f_3.__defaults__[0]=0xb765ef88 &L=0xb765ef88
[0, 1]
[1, 1, 1]
&f_3=0xb7659b68 &f_3.__defaults__[0]=0xb765ef88 &L=0xb7589028
上例中f_3默认参数L完全对应f_3.__defaults__[0],地址完全一样。
list这种是明显的可变对象,布尔常量、整型常量、字符串常量、None、tuple这些算不可变对象。函数默认参数是不可变对象时,一般不会踩这个坑。
稍微扩展一下,Python2中所谓常量数字也是对象,即PyIntObject,对于Python2,[-5,256]区间的整数已经预先创建好PyIntObject。利用ctypes可以修改这些不可变对象,若修改了[-5,256]区间的整数对象,将影响整个系统。没细究过Python3,不过实测下来也差不多。下面是Python3的测试代码
from ctypes import *#
# offset需要调整成PyIntObject.ob_ival的偏移
#
# Python2 2
# Python3 3
#
offset = sizeof( c_size_t ) * 3
addr = id( 200 ) + offset
n = c_long.from_address( addr )
print( n )
n.value = 1000
print( n )
print( 200 )
print( 200 + 1 )
>>> print( n )
c_long(200)
>>> print( n )
c_long(1000)
>>> print( 200 )
1000
>>> print( 200 + 1 )
1001
Python3测试环境中常量200已经被改成1000了,对象200不再对应数值200。从汇编级很好理解上述现象,万物皆对象,万物皆内存,不可变对象只是常规意义上的不可变。