Python 中那些令人防不胜防的坑(二)

朴灿烈づ我的快乐病毒、 2023-06-18 11:55 76阅读 0赞

在这里插入图片描述

大家好,我是 Rocky0429,一个正在学习 Python 的蒟蒻…

人不能两次踏入同一条河流,在无数次踩进同样的坑里之后,我觉得我有必要整理一下,这是 Python 防坑系列第二篇。

如果你还没读过第一篇,请点击下面链接:

Python 中那些令人防不胜防的坑(一)

这会是一个系列,每篇 5 个,系列文章更新不定,不想错过的,记得点个关注,不迷路。

0x00 嫌弃的默认可变参数

首先我们先来看一个例子:

  1. def test_func(default_arg=[]):
  2. default_arg.append('rocky0429')
  3. return default_arg

我们都知道如果调用上述函数 1 次以后所出现的结果:

  1. >>> test_func()
  2. ['rocky0429']

那么如果调用 2 次,3 次呢?你可以先自己思考一下再继续看下面的结果:

  1. >>> test_func()
  2. ['rocky0429', 'rocky0429']
  3. >>> test_func()
  4. ['rocky0429', 'rocky0429', 'rocky0429']

咦?明明我们的函数里明明对默认的可变参数赋值了,为什么第 1 次调用是初始化的状态,第 2 次,第 3 次出现的结果就不是我们想要的了呢?先别急,我们再继续看下面的调用:

  1. >>> test_func([])
  2. ['rocky0429']
  3. >>> test_func()
  4. ['rocky0429', 'rocky0429', 'rocky0429', 'rocky0429']

是不是更懵了?

其实出现这样的结果是因为 Python 中函数的默认可变参数并不是每次调用该函数时都会初始化。相反,它们会使用最近分配的值作为默认值。在上述的 test_func([]) 的结果不同是因为,当我们将明确的 [] 作为参数传递给 test_func() 的时候,就不会使用 test_func 的默认值,所以函数返回的是我们期望的值。

在自定义函数的特殊属性中,有个「 defaults」 会以元组的形式返回函数的默认参数。下面我们就用「 defaults」来演示一下,以便让大家有个更直观的感觉:

  1. >>> test_func.__defaults__ #还未调用
  2. ([],)
  3. >>> test_func() # 第 1 次
  4. ['rocky0429']
  5. >>> test_func.__defaults__ # 第 2 次的默认值
  6. (['rocky0429'],)
  7. >>> test_func() # 第 2 次
  8. ['rocky0429', 'rocky0429']
  9. >>> test_func.__defaults__ # 第 2 次的默认值
  10. (['rocky0429', 'rocky0429'],)
  11. >>> test_func([]) # 输入确定的 []
  12. ['rocky0429']
  13. >>> test_func.__defaults__ # 此时的默认值
  14. (['rocky0429', 'rocky0429'],)

那么上面那种情况该如何避免呢?毕竟我们还是希望在每次调用函数的时候都是初始化的状态的?这个也很简单,就是将 None 指定为参数的默认值,然后检查是否有值传给对应的参数。所以对于文章开始的那个例子,我们可以改成如下的形式:

  1. def test_func(default_arg=None):
  2. if not default_arg:
  3. default_arg = []
  4. default_arg.append('rocky0429')
  5. return default_arg

0x01 不一样的赋值语句

首先我们先来看一行代码:

  1. a, b = a[b] = {}, 5

看完上面的代码,现在问题来了,你知道 a,b 的值是多少么?先仔细思考一下。如果思考完毕,请继续往下看。

在交互模式中输出一下,结果如下所示:

  1. >>> a
  2. {5: ({...}, 5)}
  3. >>> b
  4. 5

怎么样?猜对了么?我猜大多数人看到这个结果都会很懵圈,就算不说结果,很多人看到最开始的那行代码,也会觉得没有头脑,下面就让我来详细的说一下,为什么是这样。

首先关于赋值语句,很多人都用过,但是更多的只是常用的形式,就是 a = b 这种模式,很少有人去看官方文档中关于赋值语句的形式:

  1. (target_list "=")+ (expression_list | yield_expression)

上面的 expression_list 是赋值语句计算表达式列表,这个可以是单个表达式或者是以逗号分割的列表(如果是后者的话,返回的是元组),并且将单个结果对象从左到右分给目标列表(target_list)中的每一项。

下面我结合这个赋值语句的形式和文章开头的代码详细说一下为什么会出现这样一个我们猜不到的结果:

首先是 (target_list “=”)+,前面好容易理解,后面带着的 + 意味着可以有一个或者多个的目标列表。在上面的代码中,目标列表就有两个:a, b 和 a[b]。这里要注意的是「表达式列表」只能有一个({}, 5)。

表达式列表计算结束后,将它的值从左到右分配给目标列表。在上面的代码中,即将 {},5 元组并赋值给 a, b,所以我们就得到了 a = {},b = 5(此处 a 被赋值的 {} 是可变对象)。

接着我们来看第二个目标列表 a[b],很多人对这个地方有困惑,觉得这个地方应该报错,因为他们觉得在之前的语句中 a 和 b 并没有被赋值。其实我们已经赋值了,我们刚将 a 赋值了 {},b 赋值了 5。

下面我们将 a 字典中 5 键的值设置为元组 ({}, 5)来创建循环引用,{…} 指的是与 a 引用了相同的对象。

下面再来看一个简单一些的循环引用的例子:

  1. >>> test_list = test_list[0] = [0]
  2. >>> test_list
  3. [[...]]
  4. >>> test_list[0]
  5. [[...]]
  6. >>> test_list[0][0][0][0] is test_list
  7. True

其实在文章最初时的那行代码中也是像这样的,比如 a[b][0] 和 a 其实是相同的对象,同样 a[b][0][b][0],a[b][0][b][0][b][0],… 都和 a 是相同的对象。

  1. >>> a[b][0][b][0] is a
  2. True
  3. >>> a[b][0] is a
  4. True

如上,我们也可以完全把文章开头的例子拆解成如下形式:

  1. a, b = {}, 5
  2. a[b] = a, b

这样,是不是更好理解一些了呢?

0x02 捕获异常不要太贪心

使用 Python 可以选择捕获哪些异常,在这里必须要注意的是不要涵盖的范围太广,即要尽量避免 except 后面为空,最好是要带东西的。except 后面如果什么也不带,它会捕捉 try 代码块中代码执行时所出现的每个异常。

虽然后面什么也不带在大多数情况下得到的也是我们想要的结果,但是代码块中如果是个嵌套结构的话,它可能会破坏嵌套结构中的 try 得到它想要的结果。比如下面这种情况:

  1. def func():
  2. try:
  3. # do something1
  4. except:
  5. # do something2
  6. try:
  7. func()
  8. except NameError:
  9. # do something3

比如上面的代码,如果在 something1 处出现了 NameError,那么所有的异常都会被 something2 处捕获到,程序就此停掉,而正常情况下应该捕获到 NameError 的 something3 处则什么异常也没有。

上面只是说了一个简单的情况,因为 Python 运行在个人电脑中,可能有时候内存错误,系统莫名退出这种异常也会被捕捉到,而现实情况是这些和我们当前的运行的程序一毛钱关系也没有。

可能这时候有人会想到 Exception 这个内置异常类,但实际情况是 except Exception 比 except 后面什么也不带好不到哪里去,大概也只是好在系统退出这种异常 Exception 不会捕捉。

那该如何使用 except 呢?

那就是尽量让 except 后面具体化,例如上面代码中的 except NameError: ,意图明确,不会拦截无关的事件。虽然只写一个 except 很方便,但有时候追求方便恰恰就是产生麻烦的源头。

0x03 循环对象

循环对象就是一个复合对象包含指向自身的引用。无论何时何地 Python 对象中检测到了循环,都会打印成 […] 的形式,而不是陷入无限循环的境地。我们还是先看一个例子:

  1. >>> lst = ['Rocky']
  2. >>> lst.append(lst)
  3. >>> lst
  4. ['Rocky', [...]]

我们除了要知道上面的 […] 代表对象中带有循环之外,还有一种容易造成误会的情况也该知道:「循环结构可能会导致程序代码陷入到无法预期的循环当中」。

至于这句话我们现在不去细究,你需要知道的是除非你真的需要,否则不要使用循环引用,我相信你肯定不想让自己陷入某些“玄学“的麻烦中。

0x04 列表重复

列表重复表面上看起来就是自己多次加上自己。这是事实,但是当列表被嵌套的时候产生的效果就不见得是我们想的那样。我们来看下面这个例子:

  1. >>> lst = [1,2,3]
  2. >>> l1 = lst * 3
  3. >>> l2 = [lst] * 3
  4. >>> l1
  5. [1, 2, 3, 1, 2, 3, 1, 2, 3]
  6. >>> l2
  7. [[1, 2, 3], [1, 2, 3], [1, 2, 3]]

上面 l1 赋值给重复四次的 lst,l2 赋值给包含重复四次 lst的。由于 lst 在 l2 的那行代码中是嵌套的,返回赋值为 lst 的原始列表,所以会出现在「赋值生成引用」这一节中出现的那种问题:

  1. >>> lst[0] = 0
  2. >>> l1
  3. [1, 2, 3, 1, 2, 3, 1, 2, 3]
  4. >>> l2
  5. [[0, 2, 3], [0, 2, 3], [0, 2, 3]]

解决上面问题和之前我们说过的一样,比如用切片的方法形成一个新的无共享的对象,因为这个的确是以另一种生成共享可变对象的方法。

另外本蒟蒻把公众号的高分原创文章整理成了一本电子书,取名《Python修炼之道》,一共 400 页!

具体内容请戳:熬夜爆肝整理 400 页 《Python 修炼之道》,一本高分原创高清电子书送给你!

目录如下:

在这里插入图片描述

现在免费送给大家,在我的公众号Python空间(微信搜 Devtogether) 回复 修炼之道即可获取。

作者Info:

【作者】:Rocky0429
【原创公众号】:Python空间。
【简介】:CSDN 博客专家, 985 计算机在读研究生,ACM 退役狗 & 亚洲区域赛银奖划水选手。这是一个坚持原创的技术公众号,每天坚持推送各种 Python 基础/进阶文章,数据分析,爬虫实战,数据结构与算法,不定期分享各类资源。
【福利】:送你新人大礼包一份,关注微信公众号,后台回复:“CSDN” 即可获取!
【转载说明】:转载请说明出处,谢谢合作!~

发表评论

表情:
评论列表 (有 0 条评论,76人围观)

还没有评论,来说两句吧...

相关阅读