Jean's Blog

一个专注软件测试开发技术的个人博客

0%

Python list和tuple的操作

列表(list)

列表(list)作为 Python 中最常用的数据类型之一,是一个可增加、删除元素的可变(mutable)容器。

基本操作

创建list

只要使用一对中括号[]。如下示例:

1
2
3
empty = []
lst = [1, 'xiaoming', 29.5, '17312662388']
lst2 = ['001', '2019-11-11', ['三文鱼', '电烤箱']]

内存示意图

  • empty 在内存中的示意图:

    image-20230524140019677

  • lst 在内存中的示意图:

    image-20230524140050260

  • lst2 在内存中的示意图:

    image-20230524140121315

获取list长度(个数)

使用 Python 的内置函数 len 获取 list 内元素个数

1
2
3
4
5
6
7
empty = []
lst = [1, 'xiaoming', 29.5, '17312662388']
lst2 = ['001', '2019-11-11', ['三文鱼', '电烤箱']]

print('Length of empty list:', len(empty))
print('Length of lst list:', len(lst))
print('Length of lst2 list:', len(lst2))

执行结果:

image-20230524140201202

遍历list并获取元素类型

遍历lst内每个元素并获取对应的类型:

  • for in对list进行遍历
  • type内置函数获取元素类型
1
2
3
4
lst = [1, 'xiaoming', 29.5, '17312662388']

for _ in lst:
print(f'{_}的类型为:{type(_)}')

执行结果:

image-20230524140226312

可以看到输出的数据类型都不同,这说明,Python的列表是不要求元素类型一致。

获取list中元素

使用索引访问list中每个位置的元素,索引从0开始,通过索引获取lst2中的元素:

1
2
3
4
5
6
lst2 = ['001', '2019-11-11', ['三文鱼', '电烤箱']]

print(lst2[0])
print(lst2[1])
print(lst2[2])
print(lst2[3]) # 越界

执行结果:

image-20230524140257044

当索引超出了范围时,Python会报一个IndexError错误,所以,要确保索引不要越界,记得最后一个元素的索引是len(classmates) - 1

也可以从列表的最后一个值取值,除了计算索引位置外,还可以用-1做索引,直接获取最后一个元素:

1
2
3
4
5
6
7
8
empty = []
lst = [1, 'xiaoming', 29.5, '17312662388']
lst2 = ['001', '2019-11-11', ['三文鱼', '电烤箱']]

print(lst2[-1])
print(lst2[-2])
print(lst2[-3])
print(lst2[-4])

执行结果:

image-20230524140317275

当然,倒数第4个就越界了。

常用列表内置函数

append

  • append() - 往list中追加元素到末尾

    1
    2
    3
    4
    >>> lst = ['a', 'b', 'c']
    >>> lst.append('d')
    >>> lst
    ['a', 'b', 'c', 'd']

insert

  • Insert(索引值, 元素) - 把元素插入到指定的位置

    1
    2
    3
    4
    >>> lst = ['a', 'b', 'c']
    >>> lst.insert(1, 'x')
    >>> lst
    ['a', 'x', 'b', 'c']

pop

  • pop() - 删除list末尾的元素

    1
    2
    3
    4
    5
    >>> lst = ['a', 'b', 'c']
    >>> lst.pop()
    'c'
    >>> lst
    ['a', 'b']
  • pop(i) - 删除指定位置的元素,i索引值

    1
    2
    3
    4
    5
    >>> lst = ['a', 'b', 'c']
    >>> lst.pop(1)
    'b'
    >>> lst
    ['a', 'c']

remove

  • 移除列表中指定的元素

    1
    2
    3
    4
    >>> lst = ['a', 'b', 'c']
    >>> lst.remove('b')
    >>> lst
    ['a', 'c']

已经学习了列表的基础操作后,下面如何要向 lst2 的第三个元素 ['三文鱼','电烤箱'] 内再增加一个元素 '烤鸭'

  • 使用列表的索引获取出该元素

    1
    2
    3
    4
    5
    lst2 = ['001', '2019-11-11', ['三文鱼', '电烤箱']]

    # sku是一个列表
    sku = lst2[2]
    print(sku)

    sku 变量位于栈帧中,同时指向 lst2[2]

    image-20230524140425531

  • 使用列表的 append 方法增加元素,append 默认增加到 sku列表尾部:

    1
    2
    3
    sku.append('烤鸭')
    print(sku)
    # 输出结果:['三文鱼', '电烤箱', '烤鸭']

    image-20230524140521822

  • 此时又想在列表的指定位置,新加入'牛腱子',使用列表的 insert 方法:

    1
    2
    3
    sku.insert(1, '牛腱子')
    print(sku)
    # 输出结果:['三文鱼', '牛腱子', '电烤箱', '烤鸭']

    image-20230524140622084

  • 这时要把'烤鸭'移除列表,使用列表的pop方法可直接移除列表尾部元素:

    1
    2
    3
    4
    5
    item = sku.pop()
    print(item)
    # 输出结果:烤鸭
    print(sku)
    # 输出结果:['三文鱼', '牛腱子', '电烤箱']

    image-20230524140712047

  • 现在又想将'三文鱼'移除,pop()能移除表尾元素,pop(i)可以移除指定的元素(通过索引值),这时我们也可以使用remove方法,移除传入的元素值

    1
    2
    3
    4
    sku.remove('三文鱼')
    # 更好使用: sku.remove(sku[0])
    print(sku)
    # 输出结果:['牛腱子', '电烤箱']

    image-20230524140744167

深浅拷贝

经过以上一系列操作后,这时我们现在输出列表lst

1
2
print(lst2)
# 输出结果:['001', '2019-11-11', ['牛腱子', '电烤箱']]

发现第三个元素也对应改变,因为 sku 引用 lst2 的第三个元素,sku 指向的内存区域改变,所以 lst2 也会相应改变。

那么,如果不想改变 lst2 的第三个元素,就需要复制出 lst2 的这个元素,列表上有 copy 方法可实现复制:

1
2
# lst2列表的初始值
lst2 = ['001','2019-11-11',['三文鱼','电烤箱']]

可视化此行代码,lst2 位于全局帧栈中,其中三个元素内存中的可视化图如下所示:

image-20230524140817254

浅拷贝(shallow copy)

这时,我们要将需要的数据拷贝出来

1
2
3
4
5
lst2 = ['001', '2019-11-11', ['三文鱼', '电烤箱']]

sku_deep = lst2[2].copy()
print(sku_deep)
# 输出结果:['三文鱼', '电烤箱']

注意,copy 函数,仅仅实现对内嵌对象的一层拷贝,属于 shallow copy。

此时可视化图为如下,因为拷贝 lst2[2],所以 sku_deep 位于栈帧中指向一块新的内存空间

image-20230524140844442

此时,再对 sku_deep 操作,便不会影响 lst2[2] 的值。

如下修改 sku_deep 的第一个元素,我们在查看意向lst2列表中的数据:

1
2
3
4
5
sku_deep[0] = '腱子'
print(sku_deep)
# 输出结果:['腱子', '电烤箱']
print(lst2)
# 输出结果:['001', '2019-11-11', ['三文鱼', '电烤箱']]

查看输出结果后,我们发现在修改,修改 sku_deep 时,不会影响 lst2[2]。

因为它们位于不同的内存空间中,如图所示,lst2[2] 中的第一个元素依然是“三文鱼”,而不是“腱子”。

image-20230524140943242

深拷贝(deepcopy)

以上已经了解了浅拷贝的流程,那么深拷贝,又有什么不同呢?

我们看下,下面的例子,a 是内嵌一层 list 的列表,对其浅拷贝生成列表 ac,修改 ac 的第三个元素,也就是列表 [3, 4, 5] 中的第二个元素为 40:

1
2
3
4
5
6
7
8
9
10
11
12
a = [1, 2, [3, 4, 5]]
print('list a:', a)
# 输出结果:list a: [1, 2, [3, 4, 5]]
ac = a.copy()
print('list ac:', ac)
# 输出结果:list ac: [1, 2, [3, 4, 5]]
ac[0] = 10
ac[2][1] = 40
print('modification ac:', ac)
# 输出结果:modification ac: [10, 2, [3, 40, 5]]
print(a[0] == ac[0])
# 输出结果:False

通过上面的流程,证明实现拷贝。

而 ac[2][1] 是否与原数组 a 的对应位置元素相等:

1
2
print(a[2][1] == ac[2][1])
# 输出结果True

返回 True,进一步证明是浅拷贝,不是深拷贝。

如下图所示:copy 只完成了一层 copy,即 [1,2, id([3,4,5])] 复制一份,而复制后,仍然指向 [3,4,5] 所在的内存空间:

image-20230524141021155

要想实现深度拷贝,需要使用 copy 模块的 deepcopy 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from copy import deepcopy

a = [1, 2, [3, 4, 5]]
print('list a:', a)
# 输出结果:list a: [1, 2, [3, 4, 5]]
ac = deepcopy(a)
print('list ac:', ac)
# 输出结果:list ac: [1, 2, [3, 4, 5]]
ac[0] = 10
ac[2][1] = 40
print('modification ac:', ac)
# 输出结果:modification ac: [10, 2, [3, 40, 5]]
print(a[0] == ac[0])
# 输出结果:False

print(a[2][1] == ac[2][1])
# 输出结果False

打印结果,都为 False,结合下图,也能看出内嵌的 list 全部完成复制,都指向了不同的内存区域。

image-20230524141102194

切片

Java 和 C++ 中,访问数组中的元素只能一次一个,但 Python 增加切片功能为访问列表带来极大便利。

首先,利用内置函数range(start, stop, step)生成序列数据,并转为list类型:

1
2
3
>>> a = list(range(1, 20, 3))
>>> a
[1, 4, 7, 10, 13, 16, 19]

使用 a[:3] 获取列表 a 的前三个元素,形象称这类操作为“切片”,切片本身也是一个列表 [1,4,7]:

1
2
>>> a[:3]
[1, 4, 7]

从以下几个例子,查看列表的切片:

  • 使用 a[-1] 获取 a 的最后一个元素,返回 int 型,值为 19;

    1
    2
    >>> a[-1]
    19
  • 使用 a[:-1] 获取除最后一个元素的切片 [1, 4, 7, 10, 13, 16];

    1
    2
    >>> a[:-1]
    [1, 4, 7, 10, 13, 16]
  • 使用 a[1:5] 生成索引为 [1,5)(不包括索引 5)的切片 [4, 7, 10, 13];

    1
    2
    >>> a[1:5]
    [4, 7, 10, 13]
  • 使用 a[1:5:2] 生成索引 [1,5) 但步长为 2 的切片 [4,10];

    1
    2
    >>> a[1:5:2]
    [4, 10]
  • 使用 a[::3] 生成索引 [0,len(a)) 步长为 3 的切片 [1,10,19];

    1
    2
    >>> a[::3]
    [1, 10, 19]
  • 使用 a[::-3] 生成逆向索引 [len(a),0) 步长为 3 的切片 [19,10,1]。

    1
    2
    >>> a[::-3]
    [19, 10, 1]

逆向:从列表最后一个元素访问到第一个元素的方向。

特别地,使用列表的逆向切片操作,只需一行代码就能逆向列表(列表元素反转):

1
2
3
4
5
6
7
8
9
a = list(range(1, 20, 3))


def reverse(lst):
return lst[::-1]


print(reverse(a))
# 输出结果:[19, 16, 13, 10, 7, 4, 1]

元组(tuple)

元组既然是不可变(immutable)对象,自然也就没有增加、删除元素的方法。

基本操作

创建tuple

使用一对括号(())就能创建一个元组对象,如:

1
2
3
a = () # 空元组对象
b = (1,'xiaoming',29.5,'17312662388')
c = ('001','2019-11-11',['三文鱼','电烤箱'])

它们都是元组,除了 list 是用 [] 创建外,其他都与 list 很相似,比如都支持切片操作。

特别注意:一个整数加一对括号,比如 (10),返回的是整数。必须加一个逗号 (10, ) 才会返回元组对象。

可变与不可变

之前说到列表是一个可变容器,元组是一个不可变容器,那么可变与不可变有什么区别呢?可变与不可变是一对很微妙的概念,详情查看总结如下:

列表可变流程查看

  • 创建一个列表 a = [1,3,[5,7],9,11,13],存储示意图:

    image-20230524141215074

  • 执行 a.pop() 后删除最后一个元素:

    image-20230524141250603

  • 列表删除后:

    image-20230524141322836

  • 接下来在索引 3 处增加一个元素 8,a.insert(3,8),插入后如下:

    image-20230524141358304

因此,对列表而言,因为它能增加或删除元素,所以它是可变的。

但是,如果仅仅在列表 a 中做这一步操作:

1
2
#在 a[2](也是一个列表)中插入元素 6
a[2].insert(1,6)

插入后可视化图:

image-20230524141435527

对于可变这个概念而言,就是真正调整a为可变的操作。

元组不可变流程查看

tuple 就是一个典型的不可变容器对象,它也没有append(),insert()这样的方法对它而言,同样也可以修改嵌套对象的取值,但这并没有真正改变 tuple 内的元素。

  • 创建一个元组 a = (1,3,[5,7],9,11,13),存储示意图:

    image-20230524141511888

  • 下面在元组中的列表插入一个元素6,可以看到,a 内元素没增没减,长度还是 6:

    image-20230524141550695

这就是不可变对象的本质,元组一旦创建后,长度就被唯一确定。

但是,对于 list 而言,列表长度会有增有减,所以它是可变的。