美文网首页
python 里那些容易踩的坑

python 里那些容易踩的坑

作者: 代码表演艺术家 | 来源:发表于2020-03-02 16:34 被阅读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  # 两次创建的对象其实是同一个
    

    相关文章

      网友评论

          本文标题:python 里那些容易踩的坑

          本文链接:https://www.haomeiwen.com/subject/pjvmkhtx.html