这篇文章总结日常开发使用列表类型的一些技巧,不断提升代码的可阅读性和优雅性。
列表是一种有序的可变容器类型,创建列表类型有两种方式,字面量语法和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']
# 切片方式获取列表元素
number = [1, 2, 3, 4, 5]
print('使用切片通过索引获取列表中对应的值:', number[0])
print('获取列表所有元素:', number[:])
print('获取列表指定开始元素到列表结束值:', number[0:])
print('获取列表开始到结束前一个值:', number[:-1])
print('获取列表最后一个值:', number[-1])
当你使用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}')
当我们处理某个列表时,一般有两个目的:修改已有成员的值,根据规则剔除某些成员。
举个例子,有个列表存放了好多的数字,我要剔除里面所有奇数,并将所有数字乘以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]
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]
上面代码演示了两种方式实现剔除奇数,并将所有数字乘以100,相比传统方式,列表推导式把几类操作压缩到了一起,结果就是代码量更少,并维持了很高的可读性。列表推导式可以算得上处理列表数据的一把利器。
[process(task) for task in tasks in not task.started]
python内置数据类型,大致上可分为可变与不可变两种。
可变:列表、字典、集合
不可变:整数、浮点数、字符串、字节串、元组
可变与不可变的区别
当我们初始化一个列表对象后,任然可以调用.append()方法来修改它的内容。而字符串和整数都是不可变的,没法修改一个已经存在的字符串对象。
下面通过一个示例展示操作可变对象和不可变对象的区别,通过这个实例说明函数参数传递机制。
在示例中分别以不可变对象的字符串和可变对象的列表演示,通过函数为两个类型的对象添加内容,观察他们添加内容前后的变化。
# 不可变对象字符串
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']
从示例的输出结果中不可变对象修改后,原始变量值没有发生改变。而列表修改内容后原始变量值发生了改变。下面解释下他们输出不同结果的原因。
如果在其他编程语言解释这两个例子,上面函数调用可以对应两种函数参数传递机制。
但是在python中没有这么复杂,Python在进行函数调用传参时,采用的既不是值传递,也不是引用传递,而是传递了“对象的引用地址”,从示例中输出的对象引用内存地址中可以看出字符串修改前后是两个内存地址,而列表修改前后是一个内存地址。
假如我们想让两个变量的修改操作互不影响,就需要拷贝变量所指向的可变对象(内存地址),做到让不同变量指向不同对象。
按拷贝深度分为两种:浅拷贝和深拷贝
大部分情况下,浅拷贝操作可以满足可变类型的复制需求,但是遇到嵌套类型数据需要用深拷贝。
# 浅拷贝
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()
浅拷贝无法拷贝嵌套数据中对象,要解决这个问题需要用到copy.deepcopy()函数进行深拷贝。深拷贝会遍历所有内容,包括嵌套的子对象。
d = [1, ['foo', 'bar'], 2]
d2 = copy.deepcopy(d)
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
使用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
使用列表时,判断成员是否存在也会存在性能陷阱,因为在列表底层使用了数组结构,所有要判断某个成员是否存在,需要从前往后遍历一遍数组,执行该操作时间复杂度为0(n) ,如果列表内容很多时间就会很长。
# 判断列表成员是否存在陷阱
def is_exist():
l = [1,2,3]
# 判断某个值是否在列表中
if 2 in l:
print('true')
print(len(l))
is_exist()
要判断列表中某个成员是否存在,使用集合比用列表更适合。
在列表中搜索有点像在一本没有目录的书中找一个单词,因为不知道在哪里只能每页的去找,时间复杂度为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()