讨论Python函数默认参数的坑

最近看到这篇

一个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]

[1, 1]

[1, 1, 1]

初看时我将信将疑,心说别不是Python2BUG,于是直接用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决定检查形参LB的地址

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

def f_1 ( L=[] ) :

    L.append( 1 )

    print( L )

    print( hex( id( L ) ) )

def f_other_1 ( B=False ) :

    print( f"B={B}" )

    print( hex( id( B ) ) )

def f_other_2 ( I=0x41414141 ) :

    print( f"I={I:#x}" )

    print( hex( id( I ) ) )

f_1()

f_1()

f_1()

f_1([0])

f_other_1()

f_other_1( True )

f_other_1()

print( hex( id( True ) ) )

print( hex( id( False ) ) )

f_other_2()

f_other_2( 0x51201314 )

f_other_2()

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

上述代码依次输出

[1]

0xb765efe8

[1, 1]

0xb765efe8

[1, 1, 1]

0xb765efe8      // 3次调用L地址始终未变

[0, 1]

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()形同闭包,闭包的特点是离开作用域时作用域内的对象不会回收销毁。

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

Spread the word. Share this post!

Meet The Author

C/ASM程序员