• 列表类型高级用法


    列表类型高级用法

    1.概述

    这篇文章总结日常开发使用列表类型的一些技巧,不断提升代码的可阅读性和优雅性。

    2.列表常用操作

    2.1.创建和读取列表元素

    1.创建列表有两种方式

    列表是一种有序的可变容器类型,创建列表类型有两种方式,字面量语法和list()内置函数。

    # 使用[]字面量来创建列表,同时还可以为列表赋值。
    number = [1, 2, 3, 4]
    print(f'使用[]字面量来创建列表,输出列表值:{number}')
    #输出结果
    使用[]字面量来创建列表,输出列表值:[1, 2, 3, 4]
    
    # 使用list()内置函数创建列表
    one_list = list('1')
    print(f'使用list()内置函数创建列表,输出列表值:{one_list}')
    #输出结果
    使用list()内置函数创建列表,输出列表值:['1']
    
    # 注意内置函数只能把任意一个可迭代的对象转为列表,也就是说不可迭代的对象不能转为列表。
    # 1是int类型,他不是一个可迭代对象,因此不能转为列表,运行后报类型错误信息:TypeError: 'int' object is not iterable
    # int_list = list(1)
    
    # 使用内置函数创建列表初始化多个值可以使用元组实现
    many_list = list(('one','two'))
    print(f'使用内置函数创建列表初始化多个值: {many_list}')
    # 输出结果
    使用内置函数创建列表初始化多个值: ['one', 'two']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    2.切片方式获取列表元素
    # 切片方式获取列表元素
    number = [1, 2, 3, 4, 5]
    print('使用切片通过索引获取列表中对应的值:', number[0])
    
    print('获取列表所有元素:', number[:])
    print('获取列表指定开始元素到列表结束值:', number[0:])
    print('获取列表开始到结束前一个值:', number[:-1])
    print('获取列表最后一个值:', number[-1])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    3.遍历列表同时获取下标

    当你使用for循环遍历列表时,默认会获取列表中所有的成员。假如在遍历列表的同时,你想获取循环的下标,可以使用内置函数
    enumerate()包裹列表对象。
    enumerate()函数适用于任何“可迭代对象” , 因此它不光可以用于列表,还可以用于元组,字典,字符串等其他对象。

    # 遍历时获取下标
    numbers = [1, 2, 3]
    for index, i in enumerate(numbers):
        print(f'列表下标:{index},列表值:{i}')
    
    # enumberate有一个可选参数,可以指定下标初始值
    for index, i in enumerate(numbers, start=10):
        print(f'列表下标:{index},列表值:{i}')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3.列表高级用法

    当我们处理某个列表时,一般有两个目的:修改已有成员的值,根据规则剔除某些成员。

    3.1.列表推导式

    1.使用列表推导式

    举个例子,有个列表存放了好多的数字,我要剔除里面所有奇数,并将所有数字乘以100。它的实现有两种方式,一个是传统方式,一个是推导式。

    numbers = [1, 2, 3, 4, 5, 6, 7]
    # 普通方式实现
    def remove_odd_mul_100(number: list):
        duble_list = []
        for i in number:
            if i % 2 == 1:
                continue
            duble_list.append(i * 100)
        return duble_list
        
    print('普通方法需要7行才能实现', remove_odd_mul_100(numbers))
    # 输出值
    普通方法需要7行才能实现 [200, 400, 600]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    numbers = [1, 2, 3, 4, 5, 6, 7]
    l = [n * 100 for n in numbers if n % 2 == 0]
    print(f'列表推导式一行就可以实现:{l}')
    # 输出值
    列表推导式一行就可以实现:[200, 400, 600]
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上面代码演示了两种方式实现剔除奇数,并将所有数字乘以100,相比传统方式,列表推导式把几类操作压缩到了一起,结果就是代码量更少,并维持了很高的可读性。列表推导式可以算得上处理列表数据的一把利器。

    2.编写列表推导式的两个不要
    • 在编写推导式过程中,不要写过于复杂的表达式,这样会影响阅读
    • 推导式的核心意义在于他会返回值,一个全新构建的列表。如果不需要这个新列表,就失去了使用表达式的意义。比如在循环中使用推导式,并不关心处理结果也就是不需要创建新的列表,直接编写循环代码会更直观。
    [process(task) for task in tasks in not task.started]
    
    • 1

    3.2.理解列表的可变性

    python内置数据类型,大致上可分为可变与不可变两种。
    可变:列表、字典、集合
    不可变:整数、浮点数、字符串、字节串、元组

    可变与不可变的区别
    当我们初始化一个列表对象后,任然可以调用.append()方法来修改它的内容。而字符串和整数都是不可变的,没法修改一个已经存在的字符串对象。

    1.可变对象与不可变对象示例

    下面通过一个示例展示操作可变对象和不可变对象的区别,通过这个实例说明函数参数传递机制。

    在示例中分别以不可变对象的字符串和可变对象的列表演示,通过函数为两个类型的对象添加内容,观察他们添加内容前后的变化。

    # 不可变对象字符串
    def add_str(in_func_obj):
        print(f'In add [before]: in_func_obj = {in_func_obj}')
        # 修改字符串对象前,对象引用内存地址
        print(f'[before] obj_id:', id(in_func_obj))
        in_func_obj += 'suffix'
        print(f'In add [after]: in_func_obj = {in_func_obj}')
        # 修改字符串对象后,对象引用内存地址
        print(f'[after] obj_id:', id(in_func_obj))
    
    
    orig_obj = 'foo'
    print(f'输出原始字符串 [before]: orig_obj = {orig_obj}')
    add_str(orig_obj)
    print(f'输出修改后的字符串 [after]: orig_obj= {orig_obj}')
    
    # 输出结果
    输出原始字符串 [before]: orig_obj = foo
    In add [before]: in_func_obj = foo
    [before] obj_id: 4371821360
    In add [after]: in_func_obj = foosuffix
    [after] obj_id: 4372015280
    输出修改后的字符串 [after]: orig_obj= foo
    
    # 修改可变对象列表
    def add_list(in_func_obj):
        print(f'In add [before]: in_func_obj = {in_func_obj}')
        # 修改字符串对象前,对象引用内存地址
        print(f'[before] obj_id:', id(in_func_obj))
        in_func_obj += ['bar']
        print(f'In add [after]: in_func_obj = {in_func_obj}')
        # 修改字符串对象后,对象引用内存地址
        print(f'[after] obj_id:', id(in_func_obj))
    
    orig_obj = ['foo']
    print(f'输出原始列表 [before]: orig_obj = {orig_obj}')
    add_list(orig_obj)
    print(f'输出修改后的列表 [after]: orig_obj= {orig_obj}')
    
    # 输出结果
    输出原始列表 [before]: orig_obj = ['foo']
    In add [before]: in_func_obj = ['foo']
    [before] obj_id: 4371818048
    In add [after]: in_func_obj = ['foo', 'bar']
    [after] obj_id: 4371818048
    输出修改后的列表 [after]: orig_obj= ['foo', 'bar']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    从示例的输出结果中不可变对象修改后,原始变量值没有发生改变。而列表修改内容后原始变量值发生了改变。下面解释下他们输出不同结果的原因。

    如果在其他编程语言解释这两个例子,上面函数调用可以对应两种函数参数传递机制。

    • 值传递:调用函数时,传过去的是变量所指向对象(值)拷贝,因此函数内变量的任何修改,都不会影响原始变量。它对应的就是不可变对象行为。
    • 引用传递:调用函数时,传过去的是变量的引用(内存地址) 因此修改函数内变量会直接影响原始变量。对应可变对象行为

    但是在python中没有这么复杂,Python在进行函数调用传参时,采用的既不是值传递,也不是引用传递,而是传递了“对象的引用地址”,从示例中输出的对象引用内存地址中可以看出字符串修改前后是两个内存地址,而列表修改前后是一个内存地址。

    3.3.深拷贝与浅拷贝

    假如我们想让两个变量的修改操作互不影响,就需要拷贝变量所指向的可变对象(内存地址),做到让不同变量指向不同对象。
    按拷贝深度分为两种:浅拷贝和深拷贝

    1.浅拷贝

    大部分情况下,浅拷贝操作可以满足可变类型的复制需求,但是遇到嵌套类型数据需要用深拷贝。

    • 通过copy模块下的copy()方法
    • 推导式创造一个浅拷贝对象
    • 容器内置构造函数实现浅拷贝对象
    • 切片方式实现浅拷贝对象
    • 容器自带浅拷贝方法
    # 浅拷贝
    nums = [1, 2, 3]
    # 通过copy模块提供的copy方法实现浅拷贝
    nums_copy = copy.copy(nums)
    
    # 推导式创造一个浅拷贝对象
    d = {'foo': 1}
    d2 = {key: value for key, value in d.items()}
    
    # 容器内置构造函数实现浅拷贝对象
    d = {'foo': 1}
    d2 = dict(d.items())
    
    # 切片方式实现浅拷贝对象
    nums = [1, 2, 3]
    nums_cut = nums[:]
    
    # 容器自带浅拷贝方法
    nums = [1, 2, 3]
    nums2 = nums.copy()
    
    d = {'foo': 1}
    d2 = d.copy()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    2.深拷贝

    浅拷贝无法拷贝嵌套数据中对象,要解决这个问题需要用到copy.deepcopy()函数进行深拷贝。深拷贝会遍历所有内容,包括嵌套的子对象。

    d = [1, ['foo', 'bar'], 2]
    d2 = copy.deepcopy(d)
    
    • 1
    • 2

    3.4.列表底层数据结构带来的性能陷阱

    1.列表使用数组存储数据

    Python在实现列表时,底层使用了数组数据结构,这种结构最大的一个特点就是当你在数组中间插入新成员时,该成员之后的每一个成员都需要移动位置。该操作的时间复杂度为0(n) ,因此在列表的头部插入数据比尾部追加数据要慢的多。在尾部追加数据时间复杂度为0(1)

    通过一是示例在测试下列表尾部追加数据和列表头部插入数据他们耗时的差距。
    从输出的结果可以看到同样是构建一个5000的列表,他们的差距居然到达了16倍。

    # 列表尾部追加数据
    def list_append():
        l = []
        for i in range(5000):
            l.append(i)
    
    # 列表头部插入数据
    def list_insert():
        l = []
        for i in range(5000):
            l.insert(0, i)
    
    
    import timeit
    
    # 测试列表尾部追加成员执行1万次耗时
    append_spent = timeit.timeit(
        setup='from __main__ import list_append',
        stmt='list_append()',
        number=10000,
    )
    print(f'list_append耗时:{append_spent}')
    
    # 测试列表插入成员执行1万次耗时
    insert_spent = timeit.timeit(
        setup='from __main__ import list_insert',
        stmt='list_insert()',
        number=10000,
    )
    print(f'list_insert耗时:{insert_spent}')
    
    # 输出结果
    list_append耗时:3.5932758282870054
    list_insert耗时:50.986891840118915
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    2.deque替代列表

    使用collections.deque类型替代列表,无论在头部还是在尾部插入数据它的时间复杂度都是0(1),因为它底层使用了双端队列。

    从结果中可以看到,使用deque以后,不论从尾部还是头部追加数据都非常的快。

    from collections import deque
    
    def deque_append():
        l = deque()
        for i in range(5000):
            l.append(i)
    
    def deque_insert():
        l = deque()
        for i in range(5000):
            l.append(i)
    
    import timeit
    
    # 测试列表尾部追加成员执行1万次耗时
    append_spent = timeit.timeit(
        setup='from __main__ import deque_append',
        stmt='deque_append()',
        number=10000,
    )
    print(f'list_append耗时:{append_spent}')
    
    # 测试列表插入成员执行1万次耗时
    insert_spent = timeit.timeit(
        setup='from __main__ import deque_insert',
        stmt='deque_insert()',
        number=10000,
    )
    print(f'list_insert耗时:{insert_spent}')
    
    # 输出结果
    list_append耗时:3.6046319701708853
    list_insert耗时:3.586874784901738
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    3.5.列表判断成员是否存在陷阱

    1.使用列表查找成员缺陷

    使用列表时,判断成员是否存在也会存在性能陷阱,因为在列表底层使用了数组结构,所有要判断某个成员是否存在,需要从前往后遍历一遍数组,执行该操作时间复杂度为0(n) ,如果列表内容很多时间就会很长。

    # 判断列表成员是否存在陷阱
    def is_exist():
        l = [1,2,3]
        # 判断某个值是否在列表中
        if 2 in l:
            print('true')
        print(len(l))
    
    is_exist()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    2.使用集合查找列表成员是否存在

    要判断列表中某个成员是否存在,使用集合比用列表更适合。
    在列表中搜索有点像在一本没有目录的书中找一个单词,因为不知道在哪里只能每页的去找,时间复杂度为0(n)。
    使用集合搜索,就像通过字典查字,它的底层使用了哈希表数据结构,要判断集合中是否存在某个对象,只需要计算出它的hash值,然后直接到该值的位置上查找是否存在即可。

    def is_set_exist():
        # 这里列表很短转换后区别不大,当列表中数据越多时效果越好
        l = [1,2,3]
        s = set(l)
        if 2 in s:
            print('true')
        print(len(l))
    is_set_exist()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  • 相关阅读:
    【Swift 60秒】60 - Creating your own structs
    【教学类-19-03】20221127《ABBABB式-规律排序-A4竖版2份》(中班)
    linux c socket编程里SO_REUSEADDR的作用
    解决 cocos2d-x-4.0 IOS构建出错的问题—执行命令出错,返回值:65
    MIT 6.5840(6.824) Lab3:Raft 设计实现
    华为电脑重装系统如何操作?电脑Win11系统重装注意什么?图文详细解答
    社交巨头与去中心化:解析Facebook在区块链的角色
    暑假到了,如何有效保护眼睛?盘点保护视力的护眼台灯
    找不到实时聊天软件?给你推荐电商企业都在用的!
    【BUG】Hexo|GET _MG_0001.JPG 404 (Not Found),hexo博客搭建过程图片路径正确却找不到图片
  • 原文地址:https://blog.csdn.net/m0_38039437/article/details/126431130