• Python可变类型的坑,不要再踩了


    B站|公众号:啥都会一点的研究生

    相关阅读

    整理了几个100%会踩的Python细节坑,提前防止脑血栓
    整理了十个100%提高效率的Python编程技巧,更上一层楼
    Python-列表,从基础到进阶用法大总结,进来查漏补缺
    Python-元组,从基础到进阶用法大总结及与列表核心区别,进来查漏补缺
    Python-字典,从基础到进阶用法大总结,进来查漏补缺
    Python-集合,从基础到进阶大总结,进来查漏补缺
    这些包括我在内都有的Python编程陋习,趁早改掉
    订阅专栏 ===> Python

    首先我们要明确的是Python中常见的类型有

    Number 数值
    String 字符串
    Tuple 元组
    List 列表
    Dictionary 字典

    其中字符串、元组、数值为不可变类型,列表、字典为可变类型,本期呢,针对列表这个可变类型,列举一些稍不注意就会踩坑的实例,先是由拷贝引起的基础坑,再是着重强调下用作函数默认参数时的细节坑,让我们一起来看看

    首先,很多情况下我们可能需要从本地读取原始数据喂入程序中,然后使用一个容器将它保存,紧接着会进行一系列处理操作让数据变成指定格式

    with open("raw_data.txt", "r") as f:
        raw_data = f.readlines()
    raw_data = [data.strip().split(" ")[0] for data in raw_data]
    
    • 1
    • 2
    • 3

    这有问题吗,当然没问题,但是如果不对原始数据进行正确拷贝,那么很有可能你在程序的某个地方对看似新拷贝的对象进行增删操作时,原始数据也跟着一起改变了,导致在其他函数使用时出错

    with open("raw_data.txt", "r") as f:
        raw_data = f.readlines()
    raw_data = [data.strip().split(" ")[0] for data in raw_data]
    
    raw_data_copy = raw_data
    '''
          processing
    '''
    while len(raw_data_copy) > 0:
        item = raw_data_copy.pop()
    '''
          processing
    '''
    print(raw_data) # []
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    如果是一维数据,可以使用copy或者切片对原始数据进行正确拷贝

    raw_data_copy = raw_data.copy()
    
    raw_data_copy = raw_data[:]
    
    • 1
    • 2
    • 3

    下一个,很多时候多维嵌套列表才是让无数练习生尽折腰,一起来看看,假设此时我们是二维数据

    with open("raw_data.txt", "r") as f:
        raw_data = f.readlines()
    raw_data = [[int(i) for i in data.strip().split(" ")] for data in raw_data]
    # [[1, 2, 3, 4, 5], [6, 7, 8, 9, 0], [2, 2, 2, 2, 2], [4, 4, 4, 4, 4], [6, 7, 1, 9, 0]]
    
    • 1
    • 2
    • 3
    • 4

    此时若使用列表内置方法copy进行拷贝,也只是完成了浅拷贝,当对其进行操作时依然会改变原始数据

    with open("raw_data.txt", "r") as f:
        raw_data = f.readlines()
    raw_data = [[int(i) for i in data.strip().split(" ")] for data in raw_data]
    # [[1, 2, 3, 4, 5], [6, 7, 8, 9, 0], [2, 2, 2, 2, 2], [4, 4, 4, 4, 4], [6, 7, 1, 9, 0]]
    
    raw_data_copy = raw_data.copy()
    raw_data_copy[0][0] = 99999
    print(raw_data)
    # [[99999, 2, 3, 4, 5], [6, 7, 8, 9, 0], [2, 2, 2, 2, 2], [4, 4, 4, 4, 4], [6, 7, 1, 9, 0]]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们可以借助copy库,使用deepcopy进行深拷贝,这样就不会影响原始数据了

    import copy
    with open("raw_data.txt", "r") as f:
        raw_data = f.readlines()
    raw_data = [[int(i) for i in data.strip().split(" ")] for data in raw_data]
    # [[1, 2, 3, 4, 5], [6, 7, 8, 9, 0], [2, 2, 2, 2, 2], [4, 4, 4, 4, 4], [6, 7, 1, 9, 0]]
    
    raw_data_copy = copy.deepcopy(raw_data)
    raw_data_copy[0][0] = 99999
    print(raw_data)
    # [[1, 2, 3, 4, 5], [6, 7, 8, 9, 0], [2, 2, 2, 2, 2], [4, 4, 4, 4, 4], [6, 7, 1, 9, 0]]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    讲完了上述例子,一起来看看当可变类型用作默认参数的问题,这也是本期主要想给大家讲的知识点,首先定义一个函数,这个函数功能是往fruit_list中添加水果,第一个参数是需新添加的fruit名,可以看到当没传入fruit_list时,默认为空的可变类型列表

    def add_fruit(fruit, fruit_list=[]):
        fruit_list.append(fruit)
        print(fruit_list)
    
    • 1
    • 2
    • 3

    我们首先传入非空fruit_list,想将西瓜添加至清单中,快速回答,这样有没有问题,这种情况下啊,是没问题的,打印也确实没问题

    def add_fruit(fruit, fruit_list=[]):
        fruit_list.append(fruit)
        print(fruit_list)
    
    fruits = ['banana', 'apple']
    
    add_fruit('watermelon', fruits)
    # ['banana', 'apple', 'watermelon']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    但是,当不传入外部创建的list,也就是直接使用函数默认空列表,想达到每调用一次函数,都往空列表传入我想添加的fruit的目的,会写成这种形式

    def add_fruit(fruit, fruit_list=[]):
        fruit_list.append(fruit)
        print(fruit_list)
    
    add_fruit('watermelon')
    add_fruit('banana')
    # ['watermelon']
    # ['watermelon', 'banana']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    打印,发现并没有如愿,我是想每次往空列表传参,但此时却往同一个列表传参,这是为什么呢,因为在Python编译的时候,默认参数是在函数定义时就被评估保存了,而不是我们每次调用函数时才进行。

    更直观一点,我们借助__defaults__这个magic method查看函数的默认值属性,打印可以看到默认为空列表,这有问题吗,这没问题,但由于他是可变的,当没有新的列表传进来时,每次都只是在它的基础上进行操作

    def add_fruit(fruit, fruit_list=[]):
        fruit_list.append(fruit)
        print(fruit_list)
        
    print(add_fruit.__defaults__) # ([],)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    不知道我有没有讲明白,为了更清晰表达,每调用一次函数,打印一次默认值属性,可以看到确实是每次都在默认值上添加新的fruit

    def add_fruit(fruit, fruit_list=[]):
        fruit_list.append(fruit)
        print(fruit_list)
        
    print(add_fruit.__defaults__) # ([],)
    add_fruit('watermelon')
    print(add_fruit.__defaults__) # (['watermelon'],)
    add_fruit('banana')
    print(add_fruit.__defaults__) # (['watermelon', 'banana'],)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    正因为其类型是可变的才会这样,正确的做法是将默认空列表改为None,然后在函数中判断如果为None,则创建一个空列表

    def add_fruit(fruit, fruit_list=None):
        if fruit_list is None:
            fruit_list = []
        fruit_list.append(fruit)
        print(fruit_list)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    同样的,我们看看它的defaults属性,打印可以看到为None

    def add_fruit(fruit, fruit_list=None):
        if fruit_list is None:
            fruit_list = []
        fruit_list.append(fruit)
        print(fruit_list)
    
    print(add_fruit.__defaults__) # (None,)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    紧接着同样每调用一次,打印一次,可以看到此时我们的目的就达到了,所以尽可能的避免将可变类型作为函数的默认参数

    def add_fruit(fruit, fruit_list=None):
        if fruit_list is None:
            fruit_list = []
        fruit_list.append(fruit)
        print(fruit_list)
    
    print(add_fruit.__defaults__) # (None,)
    add_fruit('watermelon')       # ['watermelon']
    print(add_fruit.__defaults__) # (None,)
    add_fruit('banana')           # ['banana']
    print(add_fruit.__defaults__) # (None,)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    需要强调的是,每当调用函数没有传入新列表时,都会默认使用定义函数时保存的默认值属性,所以首先自己得清楚默认参数是什么类型,一起来看看这个例子,定义一个函数用于输出日期与时间,默认参数为现在的时间

    import time
    from datetime import datetime
    
    def display_time(data=datetime.now()):
        print(data.strftime('%B %d, %Y %H:%M:%S'))
    
    • 1
    • 2
    • 3
    • 4
    • 5

    首先在不传参的情况下调用它,可以看到输出正常

    import time
    from datetime import datetime
    
    def display_time(data=datetime.now()):
        print(data.strftime('%B %d, %Y %H:%M:%S'))
        
    display_time() # November 26, 2022 16:45:50
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    OK,现在在多次不传参的情况下调用它,并且借助time.sleep让程序sleep两秒

    import time
    from datetime import datetime
    
    def display_time(data=datetime.now()):
        print(data.strftime('%B %d, %Y %H:%M:%S'))
    
    display_time() # November 26, 2022 17:04:53
    time.sleep(2)
    display_time() # November 26, 2022 17:04:53
    time.sleep(2)
    display_time() # November 26, 2022 17:04:53
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    打印发现,怎么全都一样,你到底有没有sleep,然后猛男惊醒,这不是睡不睡的事,想到了有个叫啥都生的说过函数默认值属性在定义时就已经被保存了,打印其__defaults__,终于找到了原因

    import time
    from datetime import datetime
    
    def display_time(data=datetime.now()):
        print(data.strftime('%B %d, %Y %H:%M:%S'))
    
    print(display_time.__defaults__) # (datetime.datetime(2022, 11, 26, 17, 4, 53, 360783),)
    
    display_time() # November 26, 2022 17:04:53
    time.sleep(2)
    display_time() # November 26, 2022 17:04:53
    time.sleep(2)
    display_time() # November 26, 2022 17:04:53
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    由于在定义时就已经将当时的时间作为默认,后续无论什么时候调用都是一样的结果,同样的,如果要达到本来的目的,将默认设置为None并且在程序中判断即可,再次打印就正常了

    import time
    from datetime import datetime
    
    def display_time(data=None):
        if data is None:
            data = datetime.now()
        print(data.strftime('%B %d, %Y %H:%M:%S'))
    
    print(display_time.__defaults__) # (None,)
    
    display_time() # November 26, 2022 17:10:26
    time.sleep(2)
    display_time() # November 26, 2022 17:10:28
    time.sleep(2)
    display_time() # November 26, 2022 17:10:30
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    以上就是本期的全部内容,希望有所收获,并且当再遇到这种情况时能从容应对,以上讲解有任何错误欢迎指出,我是啥都生,我们下期再见。

  • 相关阅读:
    【紧急情况】:回宿舍放下书包的我,花了20分钟敲了一个抢购脚本
    web | http 的一些问题 | get/post的区别 | http版本 | http与https的区别 | session、cookie、token
    C#程序随系统启动例子 - 开源研究系列文章
    【Spring】面向切面编程详解(AOP)
    Moka人事:实现无代码开发的API连接,打通电商平台与用户运营系统
    1.0 Zookeeper 教程
    STM32CubeMX学习笔记-CAN接口使用
    PHP指定时间戳/日期加一天,一年,一周,一月
    CRUSE: Convolutional Recurrent U-net for Speech Enhancement
    使用 NodeJS(JavaScript 和 TypeScript)使用 MS Access (MDB) 文件的 3 种方法
  • 原文地址:https://blog.csdn.net/zzh516451964zzh/article/details/128048580