python编程实践:常见的29个坑,你跳进去了没有?

Python作为一门简洁且容易上手的语言,正在受到越来越多人的喜爱。但如果你对其中的一些细节不甚了解,就有可能掉入它的“坑”里。这些坑,让你防不胜防,菜鸟经常会弄晕,而学习多年的Python老鸟也会时不时中招。本人多年教学实践使用python中,总结了菜鸟13个坑,老鸟16个坑,总计29坑。希望本文能够帮助读者避免一些常见的错误和陷阱,使学习Python编程的过程更加顺利和高效。最重要的是,通过实践和不断学习,我们将能够成为一名优秀的Python开发者。

本人对所有测试的例子进行了测试,测试环境为:win10+anaconda3+spyder+python3.9。如果您的环境不一样,有可能存在一点点小区别。

一、菜鸟容易踩的坑

1、忘记写冒号

在 if、elif、else、for、while、class、def 语句后面忘记添加 “: ”

2、误用 “=” 做等值比较

“=”是给变量赋值,“==”才是判断两个值是否相等:

3、变量没有定义

if age >= 18:
    print ('成年')
print ('ok')

由于在使用变量age之前,没有定义和赋值,这会导致:“NameError: name ‘age’ is not defined.”,改正:

age = 20
if age >= 18:
    print ('成年')
print ('ok')

4、字符串与非字符串连接

非字符串和字符串连接的时候,要将非字符串转换为字符串类型之后才能连接。

print(2 + int('2'))
#4
print('2' + str(3))
#23

5、列表的索引位置

有些初学者会习惯性地认为列表元素的位置是从 1 开始的:

name = ['cat', 'dog', 'engle']
print(name[3])

系统这时就会提示:“list index out of range.”

可别忘了,列表元素的位置是从 0 开始的。如下列所示,第 3 个元素 “mouse” 的索引位置是 2。

name = ['cat', 'dog', 'engle']
print(name[2])
#'engle'

6、使用自增 “++” 自减 “--”

用C 语言或者 Java 的,按照老习惯,习惯使用i++或者i--,但在 Python 中是没有自增自减操作符的。这是犯了经验主义的错误。

num = 0
num++

这时可以使用 “+=” 来代替 “++”。

num = 0
num += 1

7、 使用关键字命名变量

Python 3 中一共 33 个关键字:

False,None,True,and,as,assert,break,class,continue,def,del,elif,else,except,finally,for,from,global,if,import,in,is,lambda,nonlocal,not,or,pass,raise,return,try,while,with,yield

自定义变量时,变量名不能和这些关键字重复。

8、 索引元素位置时忘记调用 len 方法

通过索引位置来获取列表元素时,忘记要先使用 len 函数来获取列表的长度:

name = ['cat', 'dog', 'engle']
for i in range(name):
    print(name[i])

改正:

name = ['cat', 'dog', 'engle']
for i in range(len(name)):
    print(name[i])


9、函数中的局部变量赋值前被使用

num = 50
def test_function():
    print(num)
    num = 100
test_function()
#UnboundLocalError: local variable 'num' referenced before assignment

第一行定义了一个全局变量 num ,函数 test_function( )也定义了一个同名的局部变量,程序执行时是先查找局部变量的,在函数中找到 num 之后就不到外部查找了,此时就会出现 print 的时候变量 num 还没赋值的错误。

10、缩进问题

和其他语言的语法不同就是,Python 不能用“{}”花括号号来表示语句块,也不能用begin或end标志符来表示,而是靠缩进来区分代码块的。上下两行,对齐就是同一个代码块,不对齐,就不是同一个代码块。对齐的代码顺序执行,不对齐的代码就是另外的一个逻辑。

常见的错误用法:

  • 上下两行是一个顺序逻辑,但没有对齐。
  • 上下两行不是一个逻辑,但没有和一个顺序逻辑的代码对齐。
  • 在Python 3 中,缩进的时候,不能 Tab 和空格混用,每个缩进层次应该选择只使用 Tab 或者只使用空格。
#例子1
print("line01")
    print("line02")
#缩进会导致两个print语句是包含和被包含的关系,但这上面两行是属于同一个代码块的
 修改如下:
 print("line01")
 print("line02")
#例子2
num = 12
if num == 12:
    print('hello!')
  print('world!')
#上面的代码,两个print,没有对齐,第二个print,也没有和if对齐,而导致执行错误。
#如果两个print是一个逻辑,就应该对齐;
#如果第二个print,与if是一个顺序逻辑,就应该与if对齐。
#修改(1):
num = 12
if num == 12:
    print('hello!')
    print('world!')
##修改(2):
num = 12
if num == 12:
    print('hello!')
print('world!')

再次强调:

  • 缩进的重要性:Python使用缩进来表示代码块,缩进不正确会导致代码错误。因此,在编写Python代码时一定要注意缩进的正确性,避免因为缩进错误而导致代码执行错误。
  • 混合使用Tab和空格:Python要求使用空格进行缩进,不建议混合使用Tab和空格。因为在不同的编辑器中,Tab所占的空格数可能不同,这会导致代码缩进混乱,难以调试和阅读。

11、三元表达式

aa = 10 + 4 if False else 0
print(aa)
# 0

乍一看,按照根深蒂固的四则运算的思维,加号之前是一部分,加号之后为另一部分,结果貌似等于10。为什么打印出来的结果跟我们预想的大相径庭呢?很显然,Python解释器在遇到三元表达式时,默认把if之前的(10+4)作为三元表达式的前面部分了。这个是初学者容易出错的地方。

12、各种参数使用之坑

(1)位置参数必须一一对应,缺一不可

def f(x):
    print(f'x:{x}')
#x就是关键字参数
f(x=1)#打印结果:x:1

但是不能这样:f(x=1,2),运行会出现如下提示:

错误提示:SyntaxError: positional argument follows keyword argument

(2)关键字参数必须在位置参数右边

def ff(name='', a):
    print(f'a:{a},name:{name}')

ff(name='hello',2)

这样就是不行,关键字参数必须在位置参数的右边。正确表达如下:

def ff(a, name=''):
    print(f'a:{a},name:{name}')

ff(2, name='hello')

(3)可变关键字参数

def ff(**x):
    print(x)
ff(x=1) #打印结果:{'x': 1}
ff(x=1,y=2,width=3) #打印结果:{'x': 1, 'y': 2, 'width': 3}

但是不能这样:f(1,2),运行会出现如下提示:

错误提示:TypeError: f() takes 1 positional argument but 2 were given

13、is和==、=和==

Python中有很多运算符,例如is、=、==这三个,许多刚学的新手,不完全明白这三个运算符的意义和用法,以致于代码出错。

在 Python 中,用到对象之间比较,可以用 ==,也可以用 is,但对对象比较判断的内容并不相同,区别在哪里?

  • is 比较两个对象的 id 值是否相等,是否指向同一个内存地址,== 比较的是两个对象的内容是否相等,值是否相等;
  • =代表的含义是赋值,将某一数值赋给某个变量,比如aa=3,将3这个数值赋予给aa。
  • ==是判断是否相等,返回True或False,比如1==1。他们是相等的,那么就返回True。1==2,他们是不相等的,那么就返回False。

二、老鸟也容易踩的坑

14、含单个元素的元组

python中,小括号还能表示元组(tuple)这一数据类型, 元组是immutable的序列。Python中有些函数的参数类型为元组,其内有1个元素,这样创建是错误的:

c = (2)

它实际创建一个整型元素2,必须要在元素后加一个逗号

b = (2,)

因为在唯一的元素之后不加逗号,小括号对于Python解释器是无效的。

15、奇怪的for

for i in range(4):
    print(i)
    i = 5
#0 1 2 3 

执行的结果为:0,1,2,3,4;是不是很奇怪,执行了一次for循环之后,i就变成了5,为什么不是执行一次就退出?其实for在Python中的工作方式是这样的,range(5)生成的下一个元素就被解包,并赋值给目标列表的变量i,所以 i = 5 并不会影响循环。这个是与C语言等其他语言不一样的地方。

16、默认参数设为空

含有默认参数的函数,如果类型为容器,且设置为空:

def f(a,b=[]):
    print(b)
    return b
ret = f(1)
ret.append(1) #[]
ret.append(2) #[1, 2]

print(f(1)) #[1, 2]

再次调用f(1)时,预计打印为[],但是却为[1,2],这是可变类型的默认参数之坑,在网络上,有专家建议设置此类默认参数为None,设置None后,在python3.9下代码运行出错AttributeError: 'NoneType' object has no attribute 'append' 的提示。

参数默认值

当我们把函数的参数默认值设置为列表时,会发生什么?

def test(param=[]):
    param.append(1)
    print(param)

t1 = test()
t2 = test()

出现以上情况的原因是:默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。具体来说,函数的参数默认值保存在函数的__defaults__属性中,指向了同一个列表。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。正确的做法是设置该参数默认为None。

17、lambda表达式中使用注意

x = 10
a = lambda y: x+y
x = 20
b = lambda y: x+y
print(a(10))
#30
print(b(10))
#30

在这里a(10)和b(10)输出结果都是30,相同的原因是:x 在lambda表达式中是一个自由变量,在运行时确定,而不是定义的时候,如果需要保存 x 的值,则:

x = 10
a = lambda y, x=x: x+y
x = 20
b = lambda y, x=x: x+y
print(a(10))
#20
print(b(10))
#30

需要提请注意的是:除非有把握,个人建议一般用函数代替匿名函数lambda。

18、lambda自由参数之坑

先来看这样一段代码:

a = [lambda x: i * x for i in range(5)]
for f in a:
    print(f(2))
#8,8,8,8,8

结果并不是0,2,4,6,8,而是8,8,8,8,8。可能有不少人会觉得这是怎么啦?不着急,我们进一步分析,先试着用dis库分析字节码。分析之前需要应用包,import dis。这是个废话。

import dis
a = [lambda x: i * x for i in range(5)]
for f in a:
    #print(f(2))
    print(dis.dis(f)) 

运行结果如下:

没有得到想要的结果,只能看到参数i和x,参数i的具体值无法获取。这也就是说lambda函数在定义的时候只知道有一个i,而它的值并不明确,之后通过计算获取i的值。到这里很容易联想到闭包,因为i引用了“for i in range(5)”这个表达式中的值。接下来验证一下,我们通过f.__code__.co_freevars来获取自由变量的名称,通过f.__closure__[0].cell_contents得到自由变量的值:

a = [lambda x: i * x for i in range(5)]

for f in a:
    # print(dis.dis(f)) 
    print(f.__code__.co_freevars)
    print(f.__closure__[0].cell_contents)

运行结果如下:

从上图可以看出,自由变量i最终的值都是4,这也就解释了最开始的结果。如果还不明白可以看下面这段代码。

fs = []
for i in range(6):
    fs.append(lambda j: i*j)
print([f(2) for f in fs]) 

Python程序从上到下执行,同时它也是一门动态型的语言,举个例子,定义一个类之后,你可以动态的给它增加方法。同样,上面这个例子中,程序执行到最后i的值为5,所以lambda表达式中i为5,最终的结果为:[10, 10,10, 10, 10, 10]。

要解决上述出现的问题,就要把闭包作用域变为局部作用域:

a = [lambda x, i=i: i*x for i in range(5)]

这行代码等效于下面这种写法:

a = []
for i in range(5):
    def demo(x, i=i):
        return x * i
    a.append(demo)

再一次提请注意的是:除非有把握,个人建议一般用函数代替匿名函数lambda。

19、列表删除

删除一个列表中的元素,此元素可能在列表中重复多次:

def remove_lst(lst,n):
    for each in lst:
        if each ==n :
            lst.remove(n)
    return lst
print(remove_lst([1,5,5,5,7],5))
#[1, 5, 7]

考虑删除这个序列[1,5,5,5,7]中的元素5,结果发现只删除其中两个:[1, 5, 7]

原因是这个序列在删除的时候,动态的缩短,当你第二次循环的时候,已经跳过了一个5。正确的做法,构造成字典:

def remove_lst(lst,n):
    d = dict(zip(range(len(lst)),lst))
    return [v for k,v in d.items() if v!=n]

print(remove_lst([1,5,5,5,7],5))
#[1, 7]

20、dict怎么添加?没有insert,也没有append

list添加一个元素很容易,像下面这样:

lst = []
lst.append('hello!')  

dict怎么添加?没有insert,也没有append,怎么办?

直接写一个K-V组合,就自动添加进来了。

d = {}
d['key'] = 'value'
print(d)
#{'key': 'value'}

d[3] = 4
print(d)
#{'key': 'value', 3: 4}

怎么删除,用del,如:del(d)

21、含单个元素的元组

Python中有些函数的参数类型为元组,其内有1个元素,这样创建是错误的:

c = (2)

它实际创建一个整型元素2,必须要在元素后加一个逗号

b = (2,)

因为在唯一的元素之后不加逗号,小括号对于Python解释器是无效的。

22、共享变量未绑定

有时想要多个函数共享一个全局变量,但却在某个函数内试图修改它为局部变量:

i = 1
def f():
    i+=1 #UnboundLocalError: local variable 'i' referenced before assignment
    
def g():
    print(i)

这样,代码运行的时候会提示:UnboundLocalError: local variable 'i' referenced before assignment的错误。

应该在f函数内显示声明i为global变量:

i = 1
def f():
    global i
    i+=1
    
def g():
    print(i)

23、用isinstance代替type

type和isinstance均可用于验证Python对象类型。但是,这是一个显着的区别:在解决对象类型时,isinstance确保继承,而type则不保证继承。

24、可变的默认参数

在Python中,有两种类型的对象,在运行时,可变对象可以更改其状态或内容,而不可变对象则不能。大多数内置对象类型都是不可变的,包括int,float,string,bool和tuple。

执行函数定义后,将检查默认的Python参数一次,这意味着在定义函数时仅对表达式进行一次检查,并且对于每个后续调用都使用相同的值。但是,如果更改了可变的默认参数(列表,字典等),则所有后续调用都会更改它。

25、嵌套列表的创建

要创建一个嵌套的列表,我们可能会选择这样的方式:

lst = [[]] * 3
print(lst)
#[[], [], []]

输出:[[], [], []]

目前看起来一切正常,我们得到了一个包含3个list的嵌套list。接下来往第一个list中添加一个元素:

lst[0].append(1)
print(lst)
#[[1], [1], [1]]

输出:[[1], [1], [1]]

奇怪的事情发生了,我们只想给第一元素增加元素,结果三个list都增加了一个元素。这是因为[[]]*3并不是创建了三个不同list,而是创建了三个指向同一个list的对象,所以,当我们操作第一个元素时,其他两个元素内容也会发生变化。

如果要避免这个问题,代码如下:


lst1 = [[], [], []] 
print(lst1)
#[[], [], []]
lst1[0].append(1)
print(lst1)
#[[1], [], []]

26、try...finally + return

看下面这段代码,可以试想一下print语句打印的顺序:

import time
def bar():
    try:
        print(f'aaa {time.time()}')
        return '111', time.time()
    finally:
        print(f'bbb {time.time()}')
test = bar()
print(test)

是不是很多粉丝看到return就对print的顺序感到不知所措了,下图是最终的结果:

aaa 1694315649.8988566
bbb 1694315649.8998556
('111', 1694315649.8998556)

在try块中包含break、continue或者return语句的,在离开try块之前,finally中的语句也会被执行。所以在上面的例子中,try块return之前,会执行finally中的语句,最后再执行try中的return语句返回结果。看到这里,粉丝们是否一种豁然开朗的感觉。

27、对象销毁顺序

创建一个类OBJ:

class OBJ(object):
    def __init__(self):
        print('init')
    def __del__(self):
        print('del')

a = OBJ()
b = OBJ()
print(a is b, id(a), id(b))

创建两个OBJ示例,使用is判断是否为同一对象:

print输出结果如下

init
init
False 2110450209216 2110450210560

结果表明:a和b不是同一对象。

接下来同样创建两个对象,使用id来判断。

a = id(OBJ())
b = id(OBJ())
print(a ==b, a, b)

print输出结果如下:

init
del
init
del
True 2110450210416 2110450210416

调用id函数, Python 创建一个OBJ类的实例,并使用id函数获得内存地址后,销毁内存丢弃这个对象。当连续两次进行此操作, Python会将相同的内存地址分配给第二个对象,所以两个对象的id值是相同的。但是is行为却与之不同,通过打印顺序就可以看到。

28、了解执行时机

请看下面这个例子:

array = [1, 3, 5]
g = (x for x in array if array.count(x) > 0)
array = [5, 7, 9]
print(list(g))  #输出: [5]

结果并不是[1, 3, 5]而是[5],这有些不可思议。原因在于,in子句在声明时执行, 而条件子句则是在运行时执行。所以上面中二行代码等价于:

g = (x for x in [1, 3, 5] if [5, 7, 9].count(x) > 0)

如果程序这样修改,结果就是[1, 3, 5]。

array = [1, 3, 5]
g = []
for x in array:
    if array.count(x) > 0:
        g.append(x)

array = [5, 7, 9]
print(list(g))  #输出: [1, 3, 5]

建议:g = (x for x in array if array.count(x) > 0),虽然写法很精简,但作为我个人意见,不推荐使用。

29、相同值的不可变对象

d = {}
d[1] = 'a'
d[1.0] = 'b'
print(d) #输出:{1: 'b'}
print(d[1.0000]) #输出:b

可以看到,key=1,value=’a’的键值对神奇地消失了。这里不得不说一下Python字典是使用哈希表的思想实现的,Python 调用内部的散列函数,将键(Key)作为参数进行转换,得到一个唯一的地址,也就是哈希值。而Python 的哈希算法对相同的值计算得到的结果是一样的,这就很好地解释了上述情况出现的原因。

本文列出了在Python学习或者工作中可能会遇到的一些“坑”,虽然不见得每个人都能遇到上述问题,但是可以作为一个参考,以后就能避免踩坑了。

希望粉丝在跟Python打交道的过程中能多注意细节,甚至去了解一些内部实现的原理,这样才能更好地掌握Python这门语言。

相关文章

干就行!大牛给初学者推荐的10个Python经典案例

Python是一种高级,解释性,交互式且面向对象的脚本语言。Python的设计具有很高的可读性。它使用英语作为关键字,相对于而其他语言则使用标点符号作为语句结束不同,是依靠缩进作为结束。并且其语法结...

作为996社畜,如何自学Python?一文讲清楚

作为996社畜,应该如何自学Python?今天就给大家分享一下,工作之余,应该如何学习Python?1. 明确目标对于零基础的学员而言,要明确你学习Python仅仅是为了满足好奇心?还是有工作需要,比...

玩转Python? 一文总结30种Python的窍门和技巧

Python作为2019年必备语言之一,展现了不可替代作用。对于所有的数据科学工作者,如何提高使用Python的效率,这里,总结了30种Python的最佳实践、技巧和窍门。希望这些可以帮助大家在202...

解锁 Python 学习的正确姿势:从入门到实践

为什么选择 Python?在众多编程语言中,Python 犹如一颗璀璨的明星,脱颖而出,备受青睐。其在多个领域展现出的强大应用优势,使其成为了编程界的多面手。在数据分析领域,Python 堪称利器。它...

Linux系统Python编程实践(linux版python)

摘要:Python是一种面向对象的解释型计算机程序设计语言,具有丰富和强大的库, 广泛应用于系统管理任务的处理和Web编程。Python是由荷兰人吉多·范罗苏姆 (Guido van Rossum)于...

Python社团春学期:编程之梦,从这里启航

随着春日的阳光洒满校园,我们的Python社团也在这个充满生机的季节里,Python社团也迎来了新一轮的开学热潮。这是一个为编程爱好者提供交流、学习和实践的平台,旨在帮助同学们掌握Python编程技能...