python 里那些容易踩的坑
1. 无返回值的函数
这里最常见的就是list的反转函数reverse(), 有时候一不小心就会出现下面的错误
a=[1,2,3]
b=a.reverse()
print(b)
# 输出b为空
reverse会修改a序列本身的顺序,并不返回任何值,要让b获得a的反转版本,可以使用:b=a[::-1]
2. 可变类型复制
a=[1]
b=a
b.append(2)
print(a)
#输出
[1,2]
python数据类型分为可变(mutable)和不可变(immutable),
可变的有:list , dict,set
不可变的有:number, string, tuple
python变量存储的是数据所在的内存地址,这个内存地址指向数据本身。可变不可变指的是变量存储的内存地址处的值是否可变。对于不可变对象,当修改时,内存里会创建另一个值,然后把变量存储的内存地址修改到这个新的值上。而对于可变对象,这里内存修改值后内存地址是不变的,
上面的例子里b=a 后b和a存储的地址是一样的,都指向[1],这时候修改b修改的是b存储的地址指向的数据,内存地址不变,所以b修改的数据对a同样生效。
如果需要复制可变对象,有一下方法
# 第一种方法 使用切片slice
a=[1]
b=a[:]
b.append(2)
print(a);print(b)
#输出
[1]
[1,2]
# 第二种方法 使用copy()
a=[1]
b=a.copy()
b.append(2)
print(a);print(b)
#输出
[1]
[1,2]
上面两种都叫浅拷贝,因为这里的列表里的值都是数字,是不可变类型,如果是可变对象,这种浅拷贝就不管用了,要用深拷贝deepcopy()
# 浅拷贝
a=[ [1] ]
b=a.copy()
b[0][0]=2
print(a)
#输出
[[2]] # 这里浅拷贝已经不管用了,修改b还是会影响到a
# 深拷贝
import copy
a=[ [1] ]
b=copy.deepcopy(a)
b[0][0]=2
print(a)
#输出
[[1]]
3. a+=b 不一定等价于 a=a+b
list1 = [5, 4, 3, 2, 1]
list2 = list1
list1 += [1, 2, 3, 4]
print(list1)
print(list2)
#输出
[5, 4, 3, 2, 1, 1, 2, 3, 4]
[5, 4, 3, 2, 1, 1, 2, 3, 4]
list1 = [5, 4, 3, 2, 1]
list2 = list1
list1 = list1 + [1, 2, 3, 4]
print(list1)
print(list2)
#输出
[5, 4, 3, 2, 1, 1, 2, 3, 4]
[5, 4, 3, 2, 1]
原因在于list1 = list1 + [1, 2, 3, 4] 会生成一个新的对象,然后list1指向了新的对象,list1和list2指向的内存地址已经不同,值也就不同了。
而list1 += [1, 2, 3, 4] 只是修改了原来的对象,不会生成新的对象,变量list1 和list2 指向的还是同一个内存地址
4. dict和set的成员限制
python的字典dict是通过散列表(hash表)实现的,python通过key的哈希值快速定位到value,所以字典查找特别快。基于这点,key必须是可hashable(可hash)的,一般的不可变类型都是可hash的,而不可变类型是不可hash的,自定义的类型需要实现__eq()__方法才是可hash的。
所以,python的dict的key不可以是可变类型 list, set
>>> a={[1,2]:3}
Traceback (most recent call last):
File "<pyshell#7>", line 1, in <module>
a={[1,2]:3}
TypeError: unhashable type: 'list'
a={(1,2):3} # tuple不可变,是可hash的,可以作为key
set相对于list的很大的区别是元素不可重复,set通过计算元素的hash值来判断元素是否已经存在的,所以set的元素也要是hashable的,故可变类型也不能添加到set里
>>> set().add([1,2])
Traceback (most recent call last):
File "<pyshell#22>", line 1, in <module>
set().add([1,2])
TypeError: unhashable type: 'list'
5. 可变对象当参数
可变对象(list,dict,set)作为参数传递给函数,并且在函数内更新了参数,这个更新会影响到全局变量的
a=[1,2]
def ap(lst):
lst.append(3)
print(lst)
ap(a)
print(a)
#输出:
[1, 2, 3]
[1, 2, 3] # 全局的变量a也被修改了
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
b=1
def add(x):
x=x+1
print(x)
add(b)
print(b)
#输出:
2
1 # 不可变变量b没有被修改
这种情况有一种常见的陷阱,就是可变参数作为函数默认参数的时候
def extendlist(val, list=[]):
list.append(val)
return list
list1 = extendlist(10)
list2 = extendlist(123, ['a', 'b', 'c'])
list3 = extendlist('a')
print(list1)
print(list2)
print(list3)
#输出:
[10, 'a']
['a', 'b', 'c', 123]
[10, 'a']
上面的可变参数list在内存中一直存在,初始值为空列表,第一次调用函数的时候被修改为[10],第二次没有更新,第三次更新成了[10,'a'], 而list1和list3都指向同一个list, 所以都为[10,'a']
6. 多线程join
我们知道多线程里使用join函数会阻塞主进程,直到调用join函数的那个线程完成,主进程才会继续执行
import threading
import time
def sta():
for i in range(7):
print(i)
time.sleep(1)
t_list=[]
for i in range(3):
t=threading.Thread(target=sta)
t_list.append(t)
for t in t_list:
t.start()
t.join()
上面这段代码,看上去多个线程的sta输出会是无序的,但结果是每个线程并没有交叉执行,而是一个执行完才会执行另一个,原因就是在for循环里先执行了t.start()启动线程,紧接着就调用t.join()阻塞了主线程,造成这个for循环不会继续往下走了,要等到这个进程t执行完才会再次进入下一个for循环来创建下一个进程。相当于一个线程执行完才会启动另一个线程
所以正确的多线程调用应该是在两个for循环里分别启动start和join
for t in t_list:
t.start()
for t in t_list:
t.join()
7. 自定义类的__new__方法
python新式类里的的__init__访问不是用来创建实例对象的,创建实例对象的工作是__new__()方法来做的,而__init__()方法只是给通过__new__()方法生成的实例对象添加属性。
基于这个事实,我们经常会自定义new方法,让类在初始化的时候检查之前有没有创建过对象,从而实现单例模式,
class single:
def __new__(cls): # 自定义new方法的话一定要返回创建的实例
if not hasattr(cls, '_instance'):
#cls._instance = cls(*args,**kwargs) 调用自己会递归死循环
#cls._instance = single.__new__(cls) 这样也会递归死循环
cls._instance = super().__new__(cls) # 得调用父类的new方法才能创建实例且不死循环
return cls._instance
s1=single()
s2=single()
print(id(s1))
print(id(s2))
#输出
58688528
58688528 # 两次创建的对象其实是同一个