讨论Python函数默认参数的坑(2)

admin 2023年2月17日00:54:51评论46 views字数 2337阅读7分47秒阅读模式
创建: 2022-09-01 10:44
更新: 2022-09-02 11:41
http://scz.617.cn:8/python/202209011044.txt

接上篇,《讨论Python函数默认参数的坑》

讨论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
[01]
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。从汇编级很好理解上述现象,万物皆对象,万物皆内存,不可变对象只是常规意义上的不可变。

原文始发于微信公众号(青衣十三楼飞花堂):讨论Python函数默认参数的坑(2)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年2月17日00:54:51
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   讨论Python函数默认参数的坑(2)https://cn-sec.com/archives/1274194.html

发表评论

匿名网友 填写信息