讨论Python函数默认参数的坑

admin 2023年2月21日12:54:19评论13 views字数 2071阅读6分54秒阅读模式
创建: 2022-09-01 10:44
http://scz.617.cn:8/python/202209011044.txt

最近看到这篇

一个Python Bug干倒了估值1.6亿美元的公司 - 苏宓 [2022-08-31]
https://mp.weixin.qq.com/s/d9fI1hTfX5IrXAjRI_n4tg

故事铺垫很长,本文直奔主题讨论。要点是Digg公司当年的代码中有个函数

def get_user_by_ids ( ids=[] )

中译文中说「Python只在函数第一次被评估时初始化默认参数,这意味着每次无参调用函数时都会使用同一个列表」,然后给了个测试用例

def f ( L=[] ) :
    L.append( 1 )
    print( L )

f()
f()
f()

上述代码依次输出

[1]
[11]
[111]

初看时我将信将疑,心说别不是Python2的BUG,于是直接用Python 3.9测,还真是。

琢磨了一下,这应该与传list时实际传的是指针(引用)相关。向bluerust谈及此现象,他怀疑中译文译错了,于是我去找了英文原文

Digg's v4 launch: an optimism born of necessity - Will Larson [2018-07-02]
https://lethain.com/digg-v4/

英文原文中确实这么写的

It set default values for both parameters as empty lists. This is a super reasonable thing to do! However, Python only initializes default parameters when the function is first evaluated, which means that the same list is used for every call to the function.

观察一下默认形参是布尔型的情形

def f_other ( B=False ) :
    print( B )

f_other()
f_other( True )
f_other()

上述代码依次输出

False
True
False

bluerust决定检查形参L与B的地址

def f_1 ( L=[] ) :
    L.append( 1 )
    print( L )
    printhex( id( L ) ) )

def f_other_1 ( B=False ) :
    print( f"B={B}" )
    printhex( id( B ) ) )

def f_other_2 ( I=0x41414141 ) :
    print( f"I={I:#x}" )
    printhex( id( I ) ) )

f_1()
f_1()
f_1()
f_1([0])

f_other_1()
f_other_1( True )
f_other_1()

printhex( id( True ) ) )
printhex( id( False ) ) )

f_other_2()
f_other_2( 0x51201314 )
f_other_2()

上述代码依次输出

[1]
0xb765efe8
[11]
0xb765efe8
[111]
0xb765efe8      // 前3次调用L地址始终未变
[01]
0xb765ef48

B=False
0x857e7d0       // B地址未变
B=True
0x857e7e0
B=False
0x857e7d0       // B地址未变

0x857e7e0       // 常量True的地址
0x857e7d0       // 常量False的地址

I=0x41414141
0xb764dd10      // I地址未变
I=0x51201314
0xb768f938
I=0x41414141
0xb764dd10      // I地址未变

从地址看,「Python只在函数第一次被评估时初始化默认参数」的说法好像没毛病,f_other_1()形参B是布尔型,f_other_2()形参I是整型,以默认参数调用时,地址均未变,想必就是只初始化了一次所致?

对于进行过大规模开发的Python程序员而言,这可能是常识,我和bluerust孤陋寡闻了。他很少用默认参数,我用过布尔型、整型这类默认参数,没用过list做默认参数,所以从未碰上过这个坑。

f()这个现象颠覆了我对Python作用域的认知。bluerust提到,f()的形参L是个局部变量,传默认的[]给L,该[]的作用域应该在f()中,f()结尾并没有return(L)增加L的引用计数,离开f()时L为何未被回收销毁?我俩的直觉都是应该回收销毁。

bluerust吐槽,f()形同"闭包",闭包的特点是离开作用域时作用域内的对象不会回收销毁。

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

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

发表评论

匿名网友 填写信息