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

在这里插入图片描述


你们好,我是 Rocky0429,一个正在学习 Python 的蒟蒻…web


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


若是你还没读过第一篇,请点击下面连接:express


Python 中那些使人防不胜防的坑(一)微信


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



0x00 嫌弃的默承认变参数


首先咱们先来看一个例子:app


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

咱们都知道若是调用上述函数 1 次之后所出现的结果:svg


>>> test_func()
['rocky0429']

那么若是调用 2 次,3 次呢?你能够先本身思考一下再继续看下面的结果:函数


>>> test_func()
['rocky0429', 'rocky0429']
>>> test_func()
['rocky0429', 'rocky0429', 'rocky0429']

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


>>> test_func([])
['rocky0429']
>>> test_func()
['rocky0429', 'rocky0429', 'rocky0429', 'rocky0429']

是否是更懵了?3d


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


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


>>> test_func.__defaults__ #还未调用
([],)
>>> test_func() # 第 1 次
['rocky0429']
>>> test_func.__defaults__ # 第 2 次的默认值
(['rocky0429'],)
>>> test_func() # 第 2 次
['rocky0429', 'rocky0429']
>>> test_func.__defaults__ # 第 2 次的默认值
(['rocky0429', 'rocky0429'],)
>>> test_func([]) # 输入肯定的 []
['rocky0429']
>>> test_func.__defaults__ # 此时的默认值
(['rocky0429', 'rocky0429'],)

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


def test_func(default_arg=None):
   if not default_arg:
       default_arg = []
   default_arg.append('rocky0429')
   return default_arg


0x01 不同的赋值语句


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


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

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


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


>>> a
{5: ({...}, 5)}
>>> b
5

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


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


(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 引用了相同的对象。


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


>>> test_list = test_list[0] = [0]
>>> test_list
[[...]]
>>> test_list[0]
[[...]]
>>> test_list[0][0][0][0] is test_list
True

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


>>> a[b][0][b][0] is a
True
>>> a[b][0] is a
True

如上,咱们也能够彻底把文章开头的例子拆解成以下形式:


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

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



0x02 捕获异常不要太贪心


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


虽而后面什么也不带在大多数状况下获得的也是咱们想要的结果,可是代码块中若是是个嵌套结构的话,它可能会破坏嵌套结构中的 try 获得它想要的结果。好比下面这种状况:


def func():
try:
# do something1
except:
# do something2

try:
func()
except NameError:
# do something3

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


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


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


那该如何使用 except 呢?


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



0x03 循环对象


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


>>> lst = ['Rocky']
>>> lst.append(lst)
>>> lst
['Rocky', [...]]

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


至于这句话咱们如今不去细究,你须要知道的是除非你真的须要,不然不要使用循环引用,我相信你确定不想让本身陷入某些“玄学“的麻烦中。



0x04 列表重复


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


>>> lst = [1,2,3]
>>> l1 = lst * 3
>>> l2 = [lst] * 3
>>> l1
[1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> l2
[[1, 2, 3], [1, 2, 3], [1, 2, 3]]

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


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

解决上面问题和以前咱们说过的同样,好比用切片的方法造成一个新的无共享的对象,由于这个的确是以另外一种生成共享可变对象的方法。



做者Info:

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