Python list 二三事

Python中的 list是一种有序的集合,它对其中的元素的类型没什么要求,几乎万物皆可放list。这里讨论list的四个常用操作:
  1. 如何从list中删除元素;
  2. list的索引和切片是深拷贝还是浅拷贝;
  3. 两个list的交、并、差、对称差集;
  4. list的排序方法。

1. 删除list中的某个元素

删除list中的某个元素,可以使用delremove,del是按照下标删除元素,remove是按照值删除元素,只能删除匹配某值的第一个元素。

list1 = ['a','b','hello','world','hello'] # 按照下标删除元素
del list1[2]
print(list1)
['a', 'b', 'world', 'hello']

list1 = ['a','b','hello','world','hello'] 
list1.remove('hello') # 按照值删除元素,只能删除匹配某值的第一个元素
print(list1)
['a', 'b', 'world', 'hello']

如果想要删除list中所有的“hello”,不能像下面示意的方法使用for循环,否则如果list中有连续的“hello”会删除不掉。

list1 = ['a','b','hello','world','c','hello','hello','d']
for i in list1:
    if i == 'hello':
        list1.remove(i)
print(list1)

['a', 'b', 'world', 'c', 'hello', 'd']

从结果上看,在for循环时,当remove删除一个匹配的元素后,i已经指向了下一个元素(这和C语言里vector的迭代器一样),所以如果遇到连续两个“hello”,i就跳过了第二个“hello”。验证如下,after remove后i的值发生了变化:

list1 = ['a','b','hello','world','c','hello','hello','d']
for i in list1:
    print('current item : '+i)
    if i == 'hello':
        list1.remove(i)
        print('after remove : '+i)
print(list1)

current item : a
current item : b
current item : hello
after remove : hello
current item : c
current item : hello
after remove : hello
current item : d
['a', 'b', 'world', 'c', 'hello', 'd']

如果需要删除list中所有匹配的元素,可以做一个list的深拷贝用于遍历,而原list用于删除元素,示例如下:

from copy import deepcopy
list1 = ['a','b','hello','world','c','hello','hello','d']
list2 = deepcopy(list1)
for i in list2:
    print('current item : '+i)
    if i == 'hello':
        list1.remove(i)
        print('after remove : '+i)
print(list1)

current item : a
current item : b
current item : hello
after remove : hello
current item : world
current item : c
current item : hello
after remove : hello
current item : hello
after remove : hello
current item : d
['a', 'b', 'world', 'c', 'd']

2. list的切片和索引是深拷贝还是浅拷贝?

上例提到了list的深拷贝,那么list的切片和索引是深拷贝还是浅拷贝?
首先看一下list的索引和切片的基本使用方法:Python中list的索引可以是负数,负数是逆序,逆序从-1开始。

# 下标索引和切片
list1 = ['a','b','c','d']
print(list1[0])
print(list1[1:3])
print(list1[-4:-3])

a
['b', 'c']
['a']

list的索引或者切片是深拷贝还是浅拷贝?这里需要用到一个id方法,它能够给出对象的内存地址。
Python中对list的复制,其实是复制了list的引用,原对象和新对象会指向同一块内存地址。改变其中一个list对象中的元素的值,另一个也会被改变。如下所示,list1和list2实际指向了同一个内存地址,所以一旦改变list2中的元素的值,list1也被改变了。

list1 = ['a','b','c']
list2 = list1
print(id(list1))
print(id(list2))
list2[1] = 'd'
print(list1)

140356459153200
140356459153200
['a', 'd', 'c']

想要list1和list2互不相干,一种解决方法是使用切片的方法复制原对象,这样得到的list2的内存地址确实不一样了,改变list2中元素的值,list1不会改变。

# 使用切片的方法复制
list1 = ['a','b','c']
list2 = list1[:]
print(id(list1))
print(id(list2))
list2[1] = 'd'
print(list1)

140356987974432
140356459153040
['a', 'b', 'c']

可是这样就万事无忧了吗?如果list中的对象是个复杂的结构,比如也是个list,使用切片复制的方式有没有问题呢?

list1 = [['a','b'],['c','d']]
list2 = list1[:]
print(id(list1))
print(id(list2))
list2[1][1] = 'x'
print(list1)

140356987975872
140356458496720
[['a', 'b'], ['c', 'x']]

如果遇到嵌套列表(二维数组), 即使使用切片的方法复制了list2,修改list2中的元素,list1还是会被改掉。因为list中的元素如list1[0],是个list,是个对象,也是引用,如果查看它俩的内存地址,会发现其实是一样的。

list1 = [['a','b'],['c','d']]
list2 = list1[:]
print(id(list1[0]))
print(id(list2[0]))

140356717561408
140356717561408

所以,当list中是对象时,切片后修改元素会改变原来的list中的值,保险的办法是用深拷贝

from copy import deepcopy
list1 = [['a','b'],['c','d']]
list2 = deepcopy(list1)
print('list 的内存地址:')
print(id(list1))
print(id(list2))
print('list[0] 的内存地址:')
print(id(list1[0]))
print(id(list2[0]))
list2[1][1] = 'x'
print(list1)

list 的内存地址:
140356987985824
140356987984384
list[0] 的内存地址:
140356459155120
140356451242944
[['a', 'b'], ['c', 'd']]

3. list的交、并、差、对称差集

这也是一个较为常见的问题,给出两个list,要求它们的交集、并集、差集、对称差集。这里给出几种方法,并比较性能。两个list如下:

list1 = ['hello','world','day','night','world']
list2 = ['day','hello','spring']

首先是求交集,即找出既在list1中出现,也在list2中出现的元素。这里给出三种写法,前两种借助set来实现(推荐),后一种是list遍历方法。借助set方法的话,如果原list中有多个相同的元素,将不会保留多份,list中元素的顺序也不再保留。

# 交集 
list3 = list(set(list1) & set(list2))
print(list3)

list4 = list(set(list1).intersection(set(list2)))
print(list4)

list5 = [x for x in list1 if x in list2]
print(list5)

['hello', 'day']
['hello', 'day']
['hello', 'day']

求list并集,即在list1中或者在list2中出现的元素。

# 并集 
list3 = list(set(list1) | set(list2))
print(list3)

list4 = list(set(list1).union(set(list2)))
print(list4)

list5 = list(set(list1 + list2))
print(list5)

['night', 'day', 'spring', 'hello', 'world']
['night', 'day', 'spring', 'hello', 'world']
['day', 'spring', 'night', 'hello', 'world']

求list的差集,即在list1中出现,但不在list2中的元素

# 差集
list3 = list(set(list1).difference(set(list2))) 
print(list3)

list4 = list(set(list1)-(set(list2))) 
print(list4)

# 不求唯一 保持顺序
list5 = [x for x in list1 if x not in list2]
print(list5)

['night', 'world']
['night', 'world']
['world', 'night', 'world']

求list的对称差集,只属于list1的元素和只属于list2的元素

# 对称差集
list3 = list(set(list1).symmetric_difference(set(list2))) 
print(list3)

list4 = list(set(list1)^(set(list2))) 
print(list4)

# 不求唯一 保持顺序
list5 = [x for x in list1 if x not in list2] + [x for x in list2 if x not in list1]
print(list5)

['night', 'world', 'spring']
['night', 'world', 'spring']
['world', 'night', 'world', 'spring']

性能方面,因为set内部有哈希表,所以远高于只用list处理,set的两种写法性能差异不大。这里做一个小实验,list1和list2都是有10万数字的list,用不同的方法求解其交集,并用time计时。在这个数量级上,仅用list方法性能较慢,所以如果不要求结果保留所有元素并保持原顺序,借用set是更推荐的方法。

import random

list1 = []
list2 = []
for i in range(100000):
    n = random.randint(0, 100000)
    list1.append(n)
    m = random.randint(5000, 105000)
    list2.append(m)

%%time
# 交集1
list3 = list(set(list1) & set(list2))
CPU times: user 26.4 ms, sys: 1.86 ms, total: 28.2 ms
Wall time: 27.6 ms

%%time
# 交集2
list4 = list(set(list1).intersection(set(list2)))
CPU times: user 33.5 ms, sys: 1.17 ms, total: 34.7 ms
Wall time: 34 ms

%%time
# 交集3
list5 = [x for x in list1 if x in list2]
CPU times: user 2min 20s, sys: 243 ms, total: 2min 20s
Wall time: 2min 20s

4. list的排序操作

list的排序方法可以使用内置的sort和sorted,sorted有返回值,返回排序后的列表;sort是改变list本身的顺序,无返回值。
sorted方法

list1 = [5, 2, 3, 1, 4]
list2 = sorted(list1)
print(list2)

[1, 2, 3, 4, 5]

sort方法

list1 = [5, 2, 3, 1, 4]
list1.sort()
print(list1)

[1, 2, 3, 4, 5]

还可以在排序时通过key参数来指定一个函数用于计算待比较的值,此函数将在每个元素比较前被调用,所以复杂的对象的list,可以通过指定key来排序。比如按某一个分量排序,按某一个分量的长度排序等等。

list1 = [[1,'c','hello'],[2,'a','morning'],[3,'a','cat']]
# 按元素中的某一分量排序
list1.sort(key=lambda x:x[1])
print(list1)
[[2, 'a', 'morning'], [3, 'a', 'cat'], [1, 'c', 'hello']]

# 按元素的某一个分量的函数值排序
list1.sort(key=lambda x:len(x[2]))
print(list1)
[[3, 'a', 'cat'], [1, 'c', 'hello'], [2, 'a', 'morning']]

注:排序结果是稳定的,关键key相同时,先出现在list中的元素在排序结果中也在前面。

如果list中的元素是某个class的对象,还可以通过itemgetter、attrgetter获取元素或者对象的属性,再排序。示例如下,如果list的元素本身是可以按下标索引的(例如嵌套list),可以使用itemgetter获得分量。

from operator import itemgetter
list1 = [[1,'c','hello'],[2,'a','morning'],[3,'b','cat']]
# 对可以使用下标索引的 如按第1个分量排序
list1.sort(key=itemgetter(1))
print(list1)

[[1, 'a', 'morning'], [3, 'b', 'cat'], [1, 'c', 'hello']]

如果list中是复杂的class对象,可以用attrgetter按照属性名字获取属性的值,并按此排序。举例说明,先创建一个Person对象的list:

class Person:
    def __init__(self, name, age, work):
        self.name = name
        self.age = age
        self.work = work
    def __repr__(self):
        return repr((self.name, self.age, self.work))
    
list1 = [Person('赵赵',45,'月亮中学'), Person('李李', 20, '宇宙电子厂'),Person('王王', 35, '宇宙电子厂')]    

然后按照Person的age属性排序

from operator import attrgetter
# 对对象的某个属性排序
list2 = [Person('赵赵',45,'月亮中学'), Person('李李', 20, '宇宙电子厂'),Person('王王', 35, '宇宙电子厂')]   
list2.sort(key=attrgetter('age'))
print(list2)

[('李李', 20, '宇宙电子厂'), ('王王', 35, '宇宙电子厂'), ('赵赵', 45, '月亮中学')]

itemgetter、attrgetter更方便的一点是支持多级排序,即可以传入多个key,先按第一个key排序,第一个key相同的,再按第二个key排序。

# 先按第0个元素排序,再按第1个元素排序
list1 = [[1,'c','hello'],[1, 'a','morning'],[3, 'b','cat']]
list1.sort(key=itemgetter(0,1))
print(list1)
[[1, 'a', 'morning'], [1, 'c', 'hello'], [3, 'b', 'cat']]

# 先按work排序,再按age排序
list2 = [Person('赵赵',45,'月亮中学'), Person('李李', 20, '宇宙电子厂'),Person('王王', 35, '宇宙电子厂')]   
list2.sort(key=attrgetter('work','age'))
print(list2)
[('李李', 20, '宇宙电子厂'), ('王王', 35, '宇宙电子厂'), ('赵赵', 45, '月亮中学')]

小结

本文讨论list的四个常用操作:1.如何从list中安全的删除元素;2. 当list中是复杂结构对象时,切片和索引不是深拷贝;3.借用set求解两个list的交、并、差、对称差集;4. list的多种排序方法。

我的Python版本

>>> import sys
>>> print(sys.version)
3.7.6 (default, Jan  8 2020, 13:42:34) 
[Clang 4.0.1 (tags/RELEASE_401/final)]

你可能感兴趣的