讨论Python函数默认参数的坑2.0

接上篇。

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])

————————————————————————–

我平时写代码就这么写的,但确实不知道前面那个坑,只是简单地喜欢用NoneTrue这类非可变对象做默认参数。

上述代码依次输出

[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这种是明显的可变对象,布尔常量、整型常量、字符串常量、Nonetuple这些算不可变对象。函数默认参数是不可变对象时,一般不会踩这个坑。

稍微扩展一下,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。从汇编级很好理解上述现象,万物皆对象,万物皆内存,不可变对象只是常规意义上的不可变。

版权声明
本站“技术博客”所有内容的版权持有者为绿盟科技集团股份有限公司(“绿盟科技”)。作为分享技术资讯的平台,绿盟科技期待与广大用户互动交流,并欢迎在标明出处(绿盟科技-技术博客)及网址的情形下,全文转发。
上述情形之外的任何使用形式,均需提前向绿盟科技(010-68438880-5462)申请版权授权。如擅自使用,绿盟科技保留追责权利。同时,如因擅自使用博客内容引发法律纠纷,由使用者自行承担全部法律责任,与绿盟科技无关。

Spread the word. Share this post!

Meet The Author

C/ASM程序员