Python避坑指南

Python是一门清晰易读的语言,Guido van Rossum在设计Python时希望将其设计为一种没有任何歧义和意外行为的语言。但不幸的是,依然存在某些极端情况Python的行为跟你的预期不同。这些情况往往容易被忽视,理所当然的认为Python会按预期执行,结果给程度带来很多错误和隐患。更糟糕的是,这类问题的debug还很困难。

本系列收集整理了所有Python编程中可能会遇到的坑,我将用两篇文章

教大家如何避开这些坑,写出健壮、高效的Python代码。

本系列会深入到Python的内部,网上很少有人提及到这部分内容,建议大家收藏。

在这里插入图片描述

文章目录

列表生成的坑

我们先看一个例子:

li = [[]] * 3
print(li)
# Out: [[], [], []]

上面的代码用乘法语法创建嵌套列表,输出应该是包含3个空列表的列表。这跟我们的预期相符,完全没有问题。

接下来我们向列表中的第一个元素添加一个数字1

li[0].append(1)
print(li)
# Out: [[1], [1], [1]]

按照常理,输出应该是[[1], [], []],但是很不幸,Python的输出与我们的预期并不一致,上面的代码输出[[1], [1], [1]]。为什么会这样?

这是因为[[]] * 3并不会创建3个不同的列表,而是只创建一个列表,然后返回这个列表的3个引用。因此当我们向li[0]中追加数据时,3个引用指向同一个列表,所以三处都发生了改变。

我们可以通过输出列表元素地址进一步证实上面的解释:

li = [[]] * 3
print([id(inner_list) for inner_list in li])
# Out: [1984412007296, 1984412007296, 1984412007296]

从输出可以清楚地看到,li中3个子列表的地址是相同的,说明他们指向同一对象。-

在这里插入图片描述-

要想让生成的列表中的三个子列表是三个不同对象,我们可以这样写:

li = [[] for _ in range(3)]

上面的代码不再只创建一个列表然后返回3个引用,而是创建3个不同列表。我们同样可以通过输出地址来验证:

print([id(inner_list) for inner_list in li])
# Out: [1984411997888, 1984412000704, 1984411999552]

从输出可以看到,列表中的3个子列表地址都不同,说明是3个不同的列表。

如果你不嫌麻烦,你也可以新建一个列表,然后一个一个加入空列表,代码如下:

li = []
li.append([])
li.append([])
li.append([])
print([id(inner_list) for inner_list in li])
# Out: [1984412008256, 1984411997888, 1984412000704]

参数默认值的坑

将函数参数的默认值设为可变类型会存在潜在问题,请看下面的例子:

def foo(li=[]):
	li.append(1)
	print(li)
    
foo([2])
# Out: [2, 1]
foo([3])
# Out: [3, 1]

上面代码的输出都与我们预期相符。但如果我们调用foo()但不传入参数时会怎样呢?

foo()
# Out: [1] 		符合预期...
foo()
# Out: [1, 1]	不符合预期...

这是因为函数参数的默认值是在定义时初始化的,而不是在运行时初始化的。因此我们只有一个li列表的实例。

要解决这个问题需要将参数默认值换成不可变类型:

def foo(li=None):
	if li is None:
		li = []
	li.append(1)
 	print(li)
    
foo()
# Out: [1]
foo()
# Out: [1]

迭代过程中改变迭代序列的坑

切忌不要在for循环中改变遍历序列,尤其是增加或删除系列元素。

遍历中删除元素

请看下面的例子:

alist = [0, 1, 2]
for index, value in enumerate(alist):
	alist.pop(index) # pop方法用于从列表中移除指定位置的元素
print(alist)
# Out: [1]

你以为上面的代码会依次将列表alist的元素删除,但输出结果为[1]。这是因为for循环会按照下标依次执行。

第一次循环index值为0,我们将alist中的第0号元素删除,此时alist=[1, 2]

第二次循环index值为1,我们将alist中的第1号元素删除,结束后alist=[1]

下图描述了整个循环过程:-

在这里插入图片描述-

引起上面问题的原因是下标会依次增加,但我们删除元素时,后面的元素会前移。避免这个问题的一种解决方法是从后往前遍历,这样删除元素时就不会发生移动,不移动下标的对应关系就不会错乱。请看下面例子:

alist = [1,2,3,4,5,6,7]
for index, item in reversed(list(enumerate(alist))):
	# 删除所有偶数
	if item % 2 == 0:
		alist.pop(index)
print(alist)
# Out: [1, 3, 5, 7]

上面的例子中我们从后往前遍历,当我们删除(或添加)列表元素时不会引起其他元素移动,所以能够如我们预期的删除列表中的元素。

遍历中添加元素

边遍历边添加元素也会引起问题,这么做会造成死循环。请看下面的例子:

alist = [0, 1, 2]
for index, value in enumerate(alist):
	# 不加这个判断就会死循环,每次循环都会有新元素增加,列表永远遍历不完
	if index == 10: 
		break 
	alist.insert(index, 'a')
print(alist)
# Out: ['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 0, 1, 2]

如果没有break判断,这个循环会一直执行下去。

处理这种情况更好的办法是创建一个新列表,然后遍历原始列表向新列表中添加元素。

遍历中修改元素

遍历列表时,我们不能利用占位元素来修改列表的值。请看下面的例子:

alist = [1,2,3,4]
for item in alist:
	if item % 2 == 0:
		item = 'even'
print(alist)
# Out: [1,2,3,4]

上面例子中,改变item的值并不会改变原始列表alist的对应元素值。你需要通过列表索引来修改列表的值。

alist = [1,2,3,4]
for index, item in enumerate(alist):
	if item % 2 == 0:
		alist[index] = 'even'
print(alist)
# Out: [1, 'even', 3, 'even']
while有时比for更好

大家可能习惯用for循环多于while循环。但是某些场景下while循环比for循环更好用。比如清空列表元素:

zlist = [0, 1, 2]
while zlist:
	zlist.pop(0)
print('After: zlist =', zlist)
# Out: After: zlist = []

上面的代码,会在zlist为空时结束循环。有时我们可能需要将列表删除到一定数量,此时我们可以用len() 让循环在指定数值结束。

zlist = [0, 1, 2]
x = 1
while len(zlist) > x:
	zlist.pop(0)
print('After: zlist =', zlist)
# Out: After: zlist = [2]

while循环我们还可以放心的在循环中处理条件分支逻辑

zlist = [1,2,3,4,5]
i = 0
while i < len(zlist):
	if zlist[i] % 2 == 0:
		zlist.pop(i)
	else:
         i += 1
print(zlist)
# Out: [1, 3, 5]

有时候我们可以使用逆向思维,删除列表中不需要的元素,可以转换为将列表中需要的元素加入一个新列表。用新列表的话,无论是for循环还是while循环都能安全的处理。下面给出的是for循环的实现:

zlist = [1,2,3,4,5]
z_temp = []
for item in zlist:
	if item % 2 != 0:
		z_temp.append(item)
zlist = z_temp
print(zlist)
# Out: [1, 3, 5]

基于上面的思想,我们可以用Python最优雅最强大的列表解析式来完成前面将列表中偶数删除的任务:

zlist = [1,2,3,4,5]
[item for item in zlist if item % 2 != 0]
# Out: [1, 3, 5]

原本几行代码才能完成的工作,用列表解析式一行就完成。所以在实际开发中要善用列表解析式带来的简洁强大的表达力。

列表解析式和for循环中的变量泄露

上一节为大家讲了for循环中的坑,最后给大家展示了列表解析式的强大。在for循环中还有一个坑,就是变量泄漏。变量泄露是什么意思?看下面两段代码你就明白了:

i = 0
a = []
for i in range(3):
	a.append(i)
print(i) 
# Outputs 2


i = 0
a = [i for i in range(3)]
print(i) 
# Outputs 0

这两段代码做的事情是一样的,但是执行结束后变量i的值却不同。其中for循环中的占位变量i在循环中是不具有局部作用域的,他与外部变量i是同一个变量,因此循环迭代外部变量i的值会改变。而列表解析式中占位变量是具有局部作用域的,列表解析式中的i与前面定义i = 0的i不是同一变量,因此执行完后不会改变外部变量i的值。

如果你用的Python版本<=2.7,执行上面的列表解析式会出现变量泄露:

# Python 2.x <= 2.7
i = 0
a = [i for i in range(3)]
print(i) 
# Outputs 2

Python2.7及一下版本中,列表解析式也存在变量泄露问题,具体请参考这里。这个问题在Python 3.x得到了解决。因此Python3.x中列表解析式是不存在变量泄露的。但for依然没有私有的局部作用域,所以依然存在变量泄露。这点也再一次印证了上一节的观点,尽量使用列表解析式来完成工作

字典是无序的

很多从C++转型的程序员会以为Python的字典也会像C++的 std::map一样,是按key的字典序排序的。而事实是Python中的字典是无序的。请看下面的例子:

myDict = {'first': 1, 'second': 2, 'third': 3}
print(myDict)
# Out: {'first': 1, 'second': 2, 'third': 3}

print([k for k in myDict])
# Out: ['second', 'third', 'first']

Python中没有内置自动将字典按key排序。然而有些时候我们需要字典记住元素的插入顺序,此时我们应该用collections.OrderedDict

from collections import OrderedDict
oDict = OrderedDict([('first', 1), ('second', 2), ('third', 3)])

print([k for k in oDict])
# Out: ['first', 'second', 'third']

这里要特别注意:当我们用一个普通字典初始化OrderedDict时,OrderedDict是不会排序的,它的功能仅仅是保留元素的插入顺序。

为了减少内存开销,Python3.6修改了字典实现,其中一处影响是当用关键字形式传参时,函数会保留传递参数的顺序。

def func(**kwargs): 
    print(kwargs.keys())

func(a=1, b=2, c=3, d=4, e=5)
# Out: dict_keys(['a', 'b', 'c', 'd', 'e'])

⚠警告:这个特性可能在未来的Python版本中移除,因此开发时不要依赖此特性。

如果开发中需要对字典的内容进行排序,就需要用Python内置函数sorted(),该函数可以对所有可迭代的对象进行排序操作。语法如下:

sorted(iterable, key=None,reverse=False)

参数说明:

  • iterable:可迭代对象,即可以用for循环进行迭代的对象;
  • key:主要是用来进行比较的元素,只有一个参数,具体的函数参数取自于可迭代对象中,用来指定可迭代对象中的一个元素来进行排序;
  • reverse:排序规则,reverse=False升序(默认),reverse=True降序。

sorted()功能很强大,对字典来说,既可以按键排序,也可以按值排序。

# 按照字典的值进行排序
sortedDict1 = sorted(myDict.items(), key=lambda x: x[1])
# 按照字典的键进行排序
sortedDict2 = sorted(myDict.items(), key=lambda x: x[0])

总结

今天先给大家介绍以上最常见的5个坑。这5个坑基本都与可变容器类型数据有关,是Python新手最容易犯错,且犯错后又最难排除的坑。上面这些内容很少有教程提价到,希望本文对你有帮助。

另外我还总结一个Python编码最佳实践,建议大家结合着本文一起阅读。

猜你喜欢

转载自blog.csdn.net/qq_40647372/article/details/135354830