• Python避坑指南


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

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

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

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

    在这里插入图片描述

    列表生成的坑

    我们先看一个例子:

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

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

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

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

    按照常理,输出应该是[[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]
    
    • 1
    • 2
    • 3

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


    在这里插入图片描述

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

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

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

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

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

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

    li = []
    li.append([])
    li.append([])
    li.append([])
    print([id(inner_list) for inner_list in li])
    # Out: [1984412008256, 1984411997888, 1984412000704]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    参数默认值的坑

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

    def foo(li=[]):
    	li.append(1)
    	print(li)
        
    foo([2])
    # Out: [2, 1]
    foo([3])
    # Out: [3, 1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

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

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

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

    def foo(li=None):
    	if li is None:
    		li = []
    	li.append(1)
     	print(li)
        
    foo()
    # Out: [1]
    foo()
    # Out: [1]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

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

    遍历中删除元素

    请看下面的例子:

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

    你以为上面的代码会依次将列表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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

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

    遍历中修改元素

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

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

    上面例子中,改变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']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    while有时比for更好

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

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

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

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

    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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    有时候我们可以使用逆向思维,删除列表中不需要的元素,可以转换为将列表中需要的元素加入一个新列表。用新列表的话,无论是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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

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

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

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

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

    i = 0
    a = []
    for i in range(3):
    	a.append(i)
    print(i) 
    # Outputs 2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    i = 0
    a = [i for i in range(3)]
    print(i) 
    # Outputs 0
    
    • 1
    • 2
    • 3
    • 4

    这两段代码做的事情是一样的,但是执行结束后变量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
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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']
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这里要特别注意:当我们用一个普通字典初始化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'])
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

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

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

    参数说明:

    • 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])
    
    • 1
    • 2
    • 3
    • 4

    总结

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

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

  • 相关阅读:
    【实践篇MySQL执行计划详解
    MQ - 08 基础篇_消费者客户端SDK设计(下)
    CSRF-跨站点请求伪造
    【scikit-learn基础】--『监督学习』之 LASSO回归
    每日一题31:数据统计之即时配送食物
    [Web安全 网络安全]-Burp Suite抓包软件‘下载‘安装‘配置‘与‘使用‘
    Geoserver中使用CQL过滤要素
    ClickHouse引擎之-MaterializeMYSQL
    ElasticSearch-head前端安装以及连接ES基本步骤(linux)
    基于nodejs+vue百鸟全科赏析网站
  • 原文地址:https://blog.csdn.net/jarodyv/article/details/127830633