社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

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

青衣十三楼飞花堂 • 2 年前 • 261 次点击  
创建: 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社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/146740
 
261 次点击