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.”,改正:
4、字符串与非字符串连接非字符串和字符串连接的时候,要将非字符串转换为字符串类型之后才能连接。 print(2 + int('2'))#4print('2' + str(3))#23 5、列表的索引位置有些初学者会习惯性地认为列表元素的位置是从 1 开始的:
系统这时就会提示:“list index out of range.” 可别忘了,列表元素的位置是从 0 开始的。如下列所示,第 3 个元素 “mouse” 的索引位置是 2。 name = ['cat', 'dog', 'engle']print(name[2])#'engle' 6、使用自增 “++” 自减 “--”用C 语言或者 Java 的,按照老习惯,习惯使用i++或者i--,但在 Python 中是没有自增自减操作符的。这是犯了经验主义的错误。
这时可以使用 “+=” 来代替 “++”。 num = 0num += 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(len(name)): print(name[i]) 9、函数中的局部变量赋值前被使用
第一行定义了一个全局变量 num ,函数 test_function( )也定义了一个同名的局部变量,程序执行时是先查找局部变量的,在函数中找到 num 之后就不到外部查找了,此时就会出现 print 的时候变量 num 还没赋值的错误。 10、缩进问题和其他语言的语法不同就是,Python 不能用“{}”花括号号来表示语句块,也不能用begin或end标志符来表示,而是靠缩进来区分代码块的。上下两行,对齐就是同一个代码块,不对齐,就不是同一个代码块。对齐的代码顺序执行,不对齐的代码就是另外的一个逻辑。 常见的错误用法:
#例子1print('line01') print('line02')#缩进会导致两个print语句是包含和被包含的关系,但这上面两行是属于同一个代码块的 修改如下: print('line01') print('line02')#例子2num = 12if num == 12: print('hello!') print('world!')#上面的代码,两个print,没有对齐,第二个print,也没有和if对齐,而导致执行错误。#如果两个print是一个逻辑,就应该对齐;#如果第二个print,与if是一个顺序逻辑,就应该与if对齐。#修改(1):num = 12if num == 12: print('hello!') print('world!')##修改(2):num = 12if num == 12: print('hello!')print('world!') 再次强调:
11、三元表达式
乍一看,按照根深蒂固的四则运算的思维,加号之前是一部分,加号之后为另一部分,结果貌似等于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(a, name=''): print(f'a:{a},name:{name}')ff(2, name='hello') (3)可变关键字参数
但是不能这样:f(1,2),运行会出现如下提示: 错误提示:TypeError: f() takes 1 positional argument but 2 were given 13、is和==、=和==Python中有很多运算符,例如is、=、==这三个,许多刚学的新手,不完全明白这三个运算符的意义和用法,以致于代码出错。 在 Python 中,用到对象之间比较,可以用 ==,也可以用 is,但对对象比较判断的内容并不相同,区别在哪里?
二、老鸟也容易踩的坑14、含单个元素的元组python中,小括号还能表示元组(tuple)这一数据类型, 元组是immutable的序列。Python中有些函数的参数类型为元组,其内有1个元素,这样创建是错误的: c = (2) 它实际创建一个整型元素2,必须要在元素后加一个逗号
因为在唯一的元素之后不加逗号,小括号对于Python解释器是无效的。 15、奇怪的forfor 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、默认参数设为空含有默认参数的函数,如果类型为容器,且设置为空:
再次调用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表达式中使用注意
在这里a(10)和b(10)输出结果都是30,相同的原因是:x 在lambda表达式中是一个自由变量,在运行时确定,而不是定义的时候,如果需要保存 x 的值,则: x = 10a = lambda y, x=x: x+yx = 20b = lambda y, x=x: x+yprint(a(10))#20print(b(10))#30 需要提请注意的是:除非有把握,个人建议一般用函数代替匿名函数lambda。 18、lambda自由参数之坑先来看这样一段代码:
结果并不是0,2,4,6,8,而是8,8,8,8,8。可能有不少人会觉得这是怎么啦?不着急,我们进一步分析,先试着用dis库分析字节码。分析之前需要应用包,import dis。这是个废话。 import disa = [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得到自由变量的值:
运行结果如下: 从上图可以看出,自由变量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 = []for i in range(5): def demo(x, i=i): return x * i a.append(demo) 再一次提请注意的是:除非有把握,个人建议一般用函数代替匿名函数lambda。 19、列表删除删除一个列表中的元素,此元素可能在列表中重复多次:
考虑删除这个序列[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,也没有appendlist添加一个元素很容易,像下面这样:
dict怎么添加?没有insert,也没有append,怎么办? 直接写一个K-V组合,就自动添加进来了。 d = {}d['key'] = 'value'print(d)#{'key': 'value'}d[3] = 4print(d)#{'key': 'value', 3: 4} 怎么删除,用del,如:del(d) 21、含单个元素的元组Python中有些函数的参数类型为元组,其内有1个元素,这样创建是错误的:
它实际创建一个整型元素2,必须要在元素后加一个逗号 b = (2,) 因为在唯一的元素之后不加逗号,小括号对于Python解释器是无效的。 22、共享变量未绑定有时想要多个函数共享一个全局变量,但却在某个函数内试图修改它为局部变量:
这样,代码运行的时候会提示:UnboundLocalError: local variable 'i' referenced before assignment的错误。 应该在f函数内显示声明i为global变量: i = 1def f(): global i i+=1 def g(): print(i) 23、用isinstance代替typetype和isinstance均可用于验证Python对象类型。但是,这是一个显着的区别:在解决对象类型时,isinstance确保继承,而type则不保证继承。 24、可变的默认参数在Python中,有两种类型的对象,在运行时,可变对象可以更改其状态或内容,而不可变对象则不能。大多数内置对象类型都是不可变的,包括int,float,string,bool和tuple。 执行函数定义后,将检查默认的Python参数一次,这意味着在定义函数时仅对表达式进行一次检查,并且对于每个后续调用都使用相同的值。但是,如果更改了可变的默认参数(列表,字典等),则所有后续调用都会更改它。 25、嵌套列表的创建要创建一个嵌套的列表,我们可能会选择这样的方式:
输出:[[], [], []] 目前看起来一切正常,我们得到了一个包含3个list的嵌套list。接下来往第一个list中添加一个元素: lst[0].append(1)print(lst)#[[1], [1], [1]] 输出:[[1], [1], [1]] 奇怪的事情发生了,我们只想给第一元素增加元素,结果三个list都增加了一个元素。这是因为[[]]*3并不是创建了三个不同list,而是创建了三个指向同一个list的对象,所以,当我们操作第一个元素时,其他两个元素内容也会发生变化。 如果要避免这个问题,代码如下:
26、try...finally + return看下面这段代码,可以试想一下print语句打印的顺序: import timedef bar(): try: print(f'aaa {time.time()}') return '111', time.time() finally: print(f'bbb {time.time()}')test = bar()print(test) 是不是很多粉丝看到return就对print的顺序感到不知所措了,下图是最终的结果:
在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输出结果如下
结果表明:a和b不是同一对象。 接下来同样创建两个对象,使用id来判断。 a = id(OBJ())b = id(OBJ())print(a ==b, a, b) print输出结果如下:
调用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子句在声明时执行, 而条件子句则是在运行时执行。所以上面中二行代码等价于:
如果程序这样修改,结果就是[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、相同值的不可变对象
可以看到,key=1,value=’a’的键值对神奇地消失了。这里不得不说一下Python字典是使用哈希表的思想实现的,Python 调用内部的散列函数,将键(Key)作为参数进行转换,得到一个唯一的地址,也就是哈希值。而Python 的哈希算法对相同的值计算得到的结果是一样的,这就很好地解释了上述情况出现的原因。 本文列出了在Python学习或者工作中可能会遇到的一些“坑”,虽然不见得每个人都能遇到上述问题,但是可以作为一个参考,以后就能避免踩坑了。 希望粉丝在跟Python打交道的过程中能多注意细节,甚至去了解一些内部实现的原理,这样才能更好地掌握Python这门语言。 |
|