本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理。
作者:王圣元
PS:如有需要Python学习资料的小伙伴可以加点击下方链接自行获取
http://t.cn/A6Zvjdun
题目:在不运行下面代码(Python 3 不是 Python 2)的情况下选择答案。
def f( x=[] ):
x.append(1)
return x
print( f(), f() )
上面代码运行结果(单选)(单选)
A. [] []
B. [1] [1]
C. [1] [1, 1]
D. [1, 1] [1, 1]
很多人选第二个吧,[1] [1]。理由如下:一开始 x 是空列表,添加一个 1 不就是 [1] 吗?然后函数 f() 运行两遍。
还有人会选第三个吧,[1] [1, 1]。能选第三个的已经很强了,至少了解列表是可更改对象(mutable object),作为函数的默认参数(default argument)每次会更新参数的默认值。很棒,但是忽略了 print() 函数的运行机制。
答案是第四个, [1, 1] [1, 1]。
如果你选了第四个
,你可以关闭页面,如果不是,请往下看,绝对能学到一些新的知识。你能需要的知识点是:
不可更改对象 VS 可更改对象
判断一个数据类型 X 是不是可更改的呢?两种方法:
我们用方法一,id() 函数,来验证整数和列表是否可更改。
先看整数 i:
i = 1
print( id(i) )
i = i + 2
print( id(i) )
'''
1607630928
1607630992
'''
整数 i 在加 1 之后的 id 和之前不一样,因此加完之后的这个 i (虽然名字没变),但是不是加前的那个 i 了,因此整数是不可更改
的。
下图给上述过程做了可视化,在 Python 中,给 i 赋值 1 其实是创建一个 PyObject(有个字段存储的值为 1),然后将变量 i 指向这个 PyObject。当更新 i = i + 2 时,其实是新创建了个 PyObject(有个字段存储的值为 3),而将变量 i 指向新的 PyObject,旧的 PyObject 最后会被“回收”。
从新建 PyObject 这个特点可看出,整数不能更改。
再看列表 l:
l = [1, 10.31]
print( id(l) )
l.append('Python')
print( id(l) )
'''
2022027856840
2022027856840
'''
列表 l 在附加 ‘Python’ 之后的 id 和之前一样,因此列表是可更改
的。
下图给上述过程做了可视化。我们发现,列表作为容器型数据,它本身的 PyObject,在添加或者删除元素的时候,没有改变。换句话说,列表是可更改的。
函数默认参数
先回顾一下题目中的代码:
def f( x=[] ):
x.append(1)
return x
在函数 f 中,x 是默认参数,默认值是空列表
。
在 Python 中
默认参数值只能被初始化一次
如果使用可更改对象作为默认参数,那么被更改后的值将一直保留。
那么下面代码的运行结果就好理解了吧(注意我先用两个 print 函数打印 f() 值)。
print( f() )
print( f() )
'''
[1]
[1, 1]
'''
在运行第一行代码时,没有给参数值,则用其默认值 [],然后添加元素 1,结果是 [1],没任何问题。
在运行第二行代码时,也没有给参数值,还是用其默认值,但这个时候默认参数的类型是可更改的列表,它在第一次运行函数 f() 的时候已经变成了 [1],而这个 [1] 就更新为默认值了。再添加元素 1,结果为 [1, 1]。
So far so good,那为什么两个 f() 一起打印出来会得到 [1,1] [1,1] 呢?
print( f(), f() )
'''
[1, 1] [1, 1]
'''
这就需要了解一下 print() 函数的细节了。
print() 函数
在 Python 3 中,print() 是个函数
(function) 而不是语句 (statement)。因此 print() 函数的所有参数要
在调用函数前先被估值
因此
print( f(), f() )
等价于
x1 = f()
x2 = f()
print( x1, x2 )
解释如下:
用一张图可视化下上述过程:
用代码验证一下,注意 id(x1) 和 id(x2) 一样,就是说 x1 和 x2 指向同一个 PyObject。
x1 = f()
x2 = f()
print( id(x1), id(x2) )
print( x1, x2 )
'''
2457681941960 2457681941960
[1, 1] [1, 1]
'''
由于我们在调用 print() 函数前就完成了对 x1 和 x2 的评估,所以它们的值都为 [1, 1]。
如果分开调用 print(f()) 呢?那么结果就是 [1] [1,1] 了。看代码:
print( f() )
print( f() )
'''
[1]
[1, 1]
'''
为什么结果不是 [1, 1] [1, 1] 呢?看下面的等价代码先:
x1 = f()
print( x1 )
x2 = f()
print( x2 )
'''
[1]
[1, 1]
'''
不难发现,我们在评估 x2 之前就把 x1 的值 [1] 打印出来了,在评估 x2 之后 x1 也更新成 [1,1] 但是没打印出来,不过我们可以验证一下是不是这样。
x1 = f()
print( x1 )
x2 = f()
print( x2 )
print( x1 )
'''
[1]
[1, 1]
[1, 1]
'''
到现在你应该明晰所有难点了吧。再回到开始,其实我们就是希望这个函数就是在传入参数的列表上添加一个元素 1,那么怎么操作呢?用 None
!
None
None 和整数、浮点数、布尔一样,是一种数据类型,而且不可更改,它的类型是 NoneType。
type(None)
'''
NoneType
'''
正因为它的不可更改性质,如果你在函数中需要传入一个默认参数值,用 None !
因此上面代码更改成:
def f( x=None ):
if x is None:
x = []
x.append(1)
return x
核心点是“如果 x 值是 None
,那么重新给 x 赋值一个空列表 []”。
再运行结果正常。
print( f(), f() )
'''
[1] [1]
'''
你看,一个小题目能引出这么多 Python 的细节知识点(如变量是指针
、PyObject
、(不)可更改对象
、函数默认参数
、print 函数内部机制
、NoneType 变量
),而且这些知识点很多人都没有深挖过。我觉得这个题目作为面试题挺合适的,不要求你能完全做对,但在分析的过程可以检查你对基本知识点的理解有多深。