大家好,我是 Rocky0429,一个正在学习 Python 的蒟蒻...
人不能两次踏入同一条河流,在无数次踩进同样的坑里之后,我觉得我有必要整理一下,这是 Python 防坑系列第二篇。
如果你还没读过第一篇,请点击下面链接:
这会是一个系列,每篇 5 个,系列文章更新不定,不想错过的,记得点个关注,不迷路。
0x00 嫌弃的默认可变参数
首先我们先来看一个例子:
def test_func(default_arg=[]):
default_arg.append('rocky0429')
return default_arg
我们都知道如果调用上述函数 1 次以后所出现的结果:
>>> 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']
是不是更懵了?
其实出现这样的结果是因为 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)中的每一项。
下面我结合这个赋值语句的形式和文章开头的代码详细说一下为什么会出现这样一个我们猜不到的结果:
下面再来看一个简单一些的循环引用的例子:
>>> 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]]
解决上面问题和之前我们说过的一样,比如用切片的方法形成一个新的无共享的对象,因为这个的确是以另一种生成共享可变对象的方法。