Python 的序列(sequence)通常指一个可迭代的容器,容器中可以存放任意类型的元素。其中列表(list)是使用的最多的容器。本文讲解了列表推导式、切片命名、列表元素排序、列表元素分组的使用方法。学习了 Python 基本的列表操作后,学习这些进阶的操作,让我们写出的代码更加优雅简洁和 pythonic 。
列表推导式
当我们想要根据某些规则来构造一个列表时,首先想到的应该是列表推导式。列表推导式简化了循环操作,例如我们想要从一个原始文件名列表中获取全部 .py
文件,在没有列表推导式的情况下,我们通常会这样做:
file_list = ['foo.py', 'bar.txt', 'spam.py', 'animal.png', 'test.py']
py_list = []
for file in file_list:
if file.endswith('.py'):
py_list.append(file)
print(py_list)
# output
['foo.py', 'spam.py', 'test.py']
而如果使用列表推导式则可简化为:
py_list = [f for f in file_list if f.endswith('.py')]
print(py_list)
# output
['foo.py', 'spam.py', 'test.py']
列表推导式的介绍网上资源很多,不再赘述。这里只强调,当你需要根据某个规则来构造一个列表时,首先应该想一想,能否使用简洁的列表推导式来实现该需求,否则再回到常规的方式。
为切片命名
Python
的列表切片使用起来非常方便,但有时也会影响代码可读性。例如有一个字符串:
record = '..........19.6..........100..........'
19.6 为产品价格,100 为产品数量,那么计算总价格为:
total_price = float(record[10:14])*int(record[24:27])
但是如果这样写,可能过一段时间我们再来读代码时已经忘记了 record[10:14]
、record[24:27]
切出来的究竟是什么?为了解决上述问题,可以给切片命个名来增强可读性。
record = '..........19.6..........100..........'
price = slice(10, 14)
count = slice(24, 27)
total_price = float(record[price])*int(record[count])
slice
接收的参数格式为 slice(stop)
、slice(start, stop[, step])
。如果只接收了一个参数,则等价于切片语法 [:stop]
,如果接收两个参数,则等价于切片语法 [start:stop]
,如果接收三个参数,则等价于切片语法 [start:stop:step]
。
排序
排序相关的任务通常由内置函数 sorted
完成。需要排序的元素一般存放在一个列表容器中,列表可以存放任意类型的元素,而 sorted
函数的 key
关键字使得我们能够轻松地指定元素排序的关键字,让排序变得异常简单。下面将给出几个常见的排序例子以说明 key
关键字的使用方法。注意 Python3
和 Python2
的排序方法不能通用,下面的例子只适用于 Python3
,Python2
的排序方法未包含在本文中。
情况一
列表中的元素已经是可比较元素,直接将列表传入 sorted
函数即可返回一个已排序列表。默认为升序排列,降序排列可以指定 reverse
参数,例如:
>>> l = [3,5,4,1,8]
>>> sorted(l)
[1, 3, 4, 5, 8]
>>> sorted(l, reverse=True)
[8, 5, 4, 3, 1]
>>>
情况二
需要排序的元素是一个元组或者字典,希望根据我指定的关键字来排序,例如有如下两个列表:
l_v1 = [('b',2),('a',1),('c',3),('d',4)]
l_v2 = [
{'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
{'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
{'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]
l_v1
是一个元组列表, l_v2
是一个字典列表。对 l_v1
我们希望根据元组中第二个元素来排序,对 l_v2
我们希望根据字典的关键字 uid
进行排序。
sorted
函数接收一个关键字参数 key
,该参数指定一个可调用函数,函数返回一个值(只要是可比较的),那么 sorted
函数将根据返回的关键字对列表中的元素进行排序。例如对上面的例子:
>>> l_v1 = [('b',2),('a',1),('c',3),('d',4)]
>>> sorted(l_v1, key=lambda x: x[1])
[('a', 1), ('b', 2), ('c', 3), ('d', 4)]
>>> l_v2 = [
{'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
{'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
{'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]
>>> sorted(l_v2, key=lambda x: x['uid'])
[{'lname': 'Cleese', 'uid': 1001, 'fname': 'John'}, {'lname': 'Beazley', 'uid': 1002, 'fname': 'David'}, {'lname': 'Jones', 'uid': 1003, 'fname': 'Brian'}, {'lname': 'Jones', 'uid': 1004, 'fname': 'Big'}]
这里 lambda
函数是一个常用的技巧。lambda
关键字后边的 x
是该函数接收的参数,冒号后边的表达式是该函数的返回值。对 l_v1
来说,传递给参数 x
的就是每一个元组,其返回元组的第二个元素用于排序;对 l_v2
来说,传递给参数 x
的就是列表中的每一个字典元素,其返回字典中 uid
对应的值用于排序。
除了使用匿名函数 lambda
这种通用的方法外,Python
标准库 operator
为我们提供了一个 itemgetter
函数替代我们写的 lambda
函数,且其性能会比使用 lambda
函数略有提升。
>>> from operator import itemgetter
>>> l_v1 = [('b',2),('a',1),('c',3),('d',4)]
>>> sorted(l_v1, key=itemgetter(1))
[('a', 1), ('b', 2), ('c', 3), ('d', 4)]
>>> l_v2 = [
{'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
{'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
{'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]
>>> sorted(l_v2, key=itemgetter('uid'))
[
{'lname': 'Cleese', 'uid': 1001, 'fname': 'John'},
{'lname': 'Beazley', 'uid': 1002, 'fname': 'David'},
{'lname': 'Jones', 'uid': 1003, 'fname': 'Brian'},
{'lname': 'Jones', 'uid': 1004, 'fname': 'Big'}
]
以上例子均是返回一个单一的值用于排序关键字,前面说过,关键字 key
接收的函数可以返回任意的可比较对象。例如在 python
中,元组是可以比较的。对元组的比较规则为首先比较元组中第一个位置上的元素,如果相等,在比较第二个位置上的元素,依次类推。回到 l_v2
的例子,假设现在需求变了,我们首先对 lname
对应的值排序,如果 lname
对应的值相等,那么再根据 fname
确定其顺序。
>>> l_v2 = [
{'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
{'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
{'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]
>>> sorted(l_v2, key=lambda x: (x['lname'], x['fname']))
[
{'lname': 'Beazley', 'uid': 1002, 'fname': 'David'},
{'lname': 'Cleese', 'uid': 1001, 'fname': 'John'},
{'lname': 'Jones', 'uid': 1004, 'fname': 'Big'},
{'lname': 'Jones', 'uid': 1003, 'fname': 'Brian'}
]
这个例子中,lambda
函数返回的不再是一个标量值,而是一个元组 (x['lname'], x['fname'])
,根据元组的比较规则,首先根据元组的第一个位置上的元素 x['lname']
的大小排序,由于列表中有两个字典其 lname
对应的值都为 Jones
,因此再根据元组第二个位置的元素 x['fname']
的值排序,由于 Big
比 Brian
要小(按字母顺序依次比较),所以 Big
排在了前面。
同样使用 itemgetter
函数也是可以的,且性能会略有提升。此外我觉得 itemgetter
比 lambda
更加简洁和可读一点。
>>> l_v2 = [
{'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
{'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
{'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]
>>> sorted(l_v2, key=itemgetter('lname', 'fname'))
[
{'lname': 'Beazley', 'uid': 1002, 'fname': 'David'},
{'lname': 'Cleese', 'uid': 1001, 'fname': 'John'},
{'lname': 'Jones', 'uid': 1004, 'fname': 'Big'},
{'lname': 'Jones', 'uid': 1003, 'fname': 'Brian'}
]
情况三
需要排序的元素是一个 Python
对象,我们希望根据其某个属性值来排序。例如一个存放 User
对象的列表如下,根据其 name
属性排序:
class User:
def __init__(self, name):
self.name = name
def __str__(self):
return 'User: %s' % self.name
__repr__ = __str__ # 为了能够让 User 在解释器中显示为 'User: name' 的格式
user_list = [User('John'), User('David'), User('Big'), User('Alen')]
方法与前面的一样,定义一个函数返回 User
的 name
属性的值,把该函数传给 sorted
的 key
参数。
>>> user_list = [User('John'), User('David'), User('Big'), User('Alen')]
>>> sorted(user_list, key=lambda x: x.name)
>>> sorted(user_list, key=lambda x: x.name)
[User: Alen, User: Big, User: David, User: John]
但是,itemgetter
方法不再起作用,取而代之的是 attrgetter
方法。
>>> sorted(user_list, key=attrgetter('name'))
[User: Alen, User: Big, User: David, User: John]
attrgetter
与 itemgetter
用法完全一致,只是 itemgetter
用于获取某个位置索引或者字典关键字的取值,而 attrgetter
用于获取对象的属性值。
PS:
sorted
返回的是原始列表的一个已排序的副本,而原始列表的顺序并没有任何变化。如果你只想就地排序(即排序原始列表本身),则直接调用list
的sort
方法即可:list.sort()
。其用法与sorted
函数一样,只是该函数没有返回值,调用后原始列表已变为一个已排序列表。
对序列中的元素进行分组
和排序类似,现想根据列表中元素的某个关键字分组,使关键字相同的元素分到同一组,并可以对分好的组进行进一步处理。例如有如下的一个列表:
rows = [
{'address': '5412 N CLARK', 'date': '07/01/2012'},
{'address': '5148 N CLARK', 'date': '07/04/2012'},
{'address': '5800 E 58TH', 'date': '07/02/2012'},
{'address': '2122 N CLARK', 'date': '07/03/2012'},
{'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'},
{'address': '1060 W ADDISON', 'date': '07/02/2012'},
{'address': '4801 N BROADWAY', 'date': '07/01/2012'},
{'address': '1039 W GRANVILLE', 'date': '07/04/2012'},
]
列表的元素为字典,现想根据字典的 date
分组,使日期( date
)相同的元素分到一个组。Python
的 itertools
模块中的 groupby
函数可以很好地解决该问题。为了使用 groupby
函数,首先需要对列表排序:
>>> from operator import itemgetter
>>> sorted_rows = sorted(rows, key=itemgetter('date'))
groupby
也和 sorted
一样有一个 key
关键字参数,其接收一个可调用函数,该函数返回的值被用做分组的关键字,其用法和 sorted
的 key
关键字参数一样 。
>>> for date, items in groupby(sorted_rows, key=itemgetter('date')):
print(date)
for i in items:
print(' ', i)
07/01/2012
{'address': '5412 N CLARK', 'date': '07/01/2012'}
{'address': '4801 N BROADWAY', 'date': '07/01/2012'}
07/02/2012
{'address': '5800 E 58TH', 'date': '07/02/2012'}
{'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'}
{'address': '1060 W ADDISON', 'date': '07/02/2012'}
07/03/2012
{'address': '2122 N CLARK', 'date': '07/03/2012'}
07/04/2012
{'address': '5148 N CLARK', 'date': '07/04/2012'}
{'address': '1039 W GRANVILLE', 'date': '07/04/2012'}
可以看到 groupby
返回的值分别是用于分组的关键字对应的值和该组的全部成员。groupby
实际返回一个生成器,通过迭代即可分别对各组进行处理。值得注意的一点是,分组前对列表排序这一步必不可少,否则对于非紧邻的元素即使其值相同也会被分在不同组。