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这门语言。