专栏首页王的机器听说你会玩 Python 系列 2 - 99% 人会做错的题

听说你会玩 Python 系列 2 - 99% 人会做错的题

本文含 2693 字,5 图表截屏

建议阅读 15 分钟

本文是听说你会玩 Python 系列的第二篇

引言

题目:在不运行下面代码(Python 3 不是 Python 2)的情况下选择答案。

def f( x=[] ):    x.append(1)    return x print( f(), f() )

很多人选第二个吧,[1] [1]。理由如下:一开始 x 是空列表,添加一个 1 不就是 [1] 吗?然后函数 f() 运行两遍。

还有人会选第三个吧,[1] [1, 1]。能选第三个的已经很强了,至少了解列表是可更改对象(mutable object),作为函数的默认参数(default argument)每次会更新参数的默认值。很棒,但是忽略了 print() 函数的运行机制。

答案是第四个, [1, 1] [1, 1]。

如果你选了第四个,你可以关闭页面,如果不是,请往下看,绝对能学到一些新的知识。你能需要的知识点是:

  1. 了解什么是不可更改对象可更改对象
  2. 了解函数的默认参数
  3. 了解在 Python 3 里面 print() 是函数而不是语句(在 Python 2 里是语句)

不可更改对象 VS 可更改对象

判断一个数据类型 X 是不是可更改的呢?两种方法:

  • 方法一:用 id(X) 函数,对 X 进行某种操作,比较操作前后的 id,如果不一样,则 X 不可更改,如果一样,则 X 可更改。
  • 方法二:用 hash(X) 函数,只要不报错,证明 X 可被哈希,即不可更改,反过来不可被哈希,即可更改。

我们用方法一,id() 函数,来验证整数和列表是否可更改。

先看整数 i:

i = 1print( id(i) )i = i + 2print( 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 )

解释如下:

  • 第一次调用 f() 产生 PyObject 并赋值给 x1 时,x1 指向 PyObject 而且其值为 [1]
  • 第二次调用 f() 赋值给 x2 时,PyObject 里的值更新为 [1, 1],而 x1 和 x2 指向它,因此两个值都更新为 [1, 1]

用一张图可视化下上述过程:

用代码验证一下,注意 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 变量),而且这些知识点很多人都没有深挖过。我觉得这个题目作为面试题挺合适的,不要求你能完全做对,但在分析的过程可以检查你对基本知识点的理解有多深。

朋友们,这道题你做对了吗?如果做错了现在学到新知识点了吗?五一快乐!

Stay Tuned!

本文分享自微信公众号 - 王的机器(MeanMachine1031),作者:王圣元

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-05-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 盘一盘 Python 系列 1 - 入门篇 (上)

    昨晚见到大神 Aurélien Géron 真人讲 Tensorflow 2.0 的 autograph,会后和他聊天得知他已经搬到新加坡了,而且在这边也有一个...

    用户5753894
  • 听说你会玩 Python 系列 1 - 六酷技巧

    不管学什么,我个人是非常喜欢小技巧(tricks)的,Python 也不例外。著名 Python 技巧大师 Dan Bader 是这样定义 Python Tri...

    用户5753894
  • 盘一盘 Python 特别篇 16 - DateTime

    在 Python 中有一个内置的专门处理“日期时间”的工具包叫做 datetime,而日期 (date) 和时间 (time) 在金融工程中的处处都用得到。

    用户5753894
  • Python入门学习(一)

    看完了莫烦Python的视频,对于Python有了一点感觉,接下来打算把小甲鱼的视频啃完,附上学习网址:http://blog.fishc.com/catego...

    闪电gogogo
  • 5.python 字符串/列表/元组/字典之间的相互转换

    字符串转为列表list,可以使用str.split()方法,split方法是在字符串中对指定字符进行切片,并返回一个列表,示例代码如下:

    猿说编程[Python和C]
  • Python 学习之异常

    Python技术与生活认知的分享
  • 字节跳动AI lab 三面记录

    三面面试官准时上线,技术总监看起来都比较严肃,哈哈哈,不过后面好亲切,谢谢面试官维持的面试氛围,感觉很不错~

    牛客网
  • Python之Numpy初识

    今天翻了下计划,要学习Numpy了,所以得调动脑细胞的积极性,看看能有什么收获。 首先得了解下什么是Numpy,从我的印象中,一般提到这个工具都会和机器学习关...

    jeanron100
  • Python基础之序列构成的数组

    容器序列能够存放不同类型的数据,比扁平序列更灵活; 扁平序列只能存放一种类型的原子性的数据,体积更小速度更快。eg:数字,字符字节

    py3study
  • C++核心准则C.162:将大致相同的操作设计为重载函数​

    Having different names for logically equivalent operations on different argument...

    面向对象思考

扫码关注云+社区

领取腾讯云代金券