分享

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

 流形sbz 2023-11-12 发布于甘肃

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

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

本人对所有测试的例子进行了测试,测试环境为: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 = 20if age >= 18:    print ('成年')print ('ok')

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

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

print(2 + int('2'))#4print('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 = 0num++

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

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(name):    print(name[i])

改正:

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

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

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

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

10、缩进问题

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

常见的错误用法:

  • 上下两行是一个顺序逻辑,但没有对齐。
  • 上下两行不是一个逻辑,但没有和一个顺序逻辑的代码对齐。
  • 在Python 3 中,缩进的时候,不能 Tab 和空格混用,每个缩进层次应该选择只使用 Tab 或者只使用空格。
#例子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!')

再次强调:

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

11、三元表达式

aa = 10 + 4 if False else 0print(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 bret = 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 = 10a = lambda y: x+yx = 20b = lambda y: x+yprint(a(10))#30print(b(10))#30

在这里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自由参数之坑

先来看这样一段代码:

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 disa = [lambda x: i * x for i in range(5)]for f in a: #print(f(2)) print(dis.dis(f))

运行结果如下:

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

没有得到想要的结果,只能看到参数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)

运行结果如下:

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

从上图可以看出,自由变量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 lstprint(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] = 4print(d)#{'key': 'value', 3: 4}

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

21、含单个元素的元组

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

c = (2)

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

b = (2,)

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

22、共享变量未绑定

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

i = 1def 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 = 1def 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 = [[]] * 3print(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 timedef 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.8988566bbb 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输出结果如下

initinitFalse 2110450209216 2110450210560

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

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

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

print输出结果如下:

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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多