python 里那些容易踩的坑

2020-03-02  本文已影响0人  代码表演艺术家
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  # 两次创建的对象其实是同一个
上一篇下一篇

猜你喜欢

热点阅读