Python3 cookbook学习笔记-数据结构与算法5

时间:2022-10-18 16:06:23

排序不支持原生比较的对象

问题:
你想排序类型相同的对象,但是他们不支持原生的比较操作。

解决方案:
内置的sorted函数有一个关键字参数key,可以传入一个callable对象给它,这个callable对象对每个传入的对象返回一个值,这个值会被sorted用来排序这些对象。

比如:

>>> class User:
... def __init__(self, user_id):
... self.user_id = user_id
... def __repr__(self):
... return 'User({})'.format(self.user_id)
...
>>> users = [User(23), User(3), User(99)]
>>> users
[User(23), User(3), User(99)]
>>> sorted(users, key=lambda u: u.user_id)
[User(3), User(23), User(99)]

另一种方式是使用operator.attrgetter()来代替lambda函数:

>>> from operator import attrgetter
>>> sorted(users, key=attrgetter('user_id'))
[User(3), User(23), User(99)]

选择使用lambda还是attrgetter可能取决于个人喜好。但是sttrgetter通常会运行地快点,并且还能允许多个字段进行比较。这个跟operator.itemgetter函数比较类似。例如,如果User实例还有一个first_name和last_name属性,那么可以如下这么排序:

by_name = sorted(users, key=attrgetter('last_name', 'first_name'))

同样,这一小节的技术,也适用于项min()和max()之类的函数。如:

>>> min(users, key=attrgetter('user_id') User(3)
>>> max(users, key=attrgetter('user_id') User(99)
>>>

通过某个字段将记录分组

问题:
你有一个字典或者实例的序列,然后你想根据某个特定的字段比如date来分组迭代访问。

解决方案:

itertools.groupby()函数对于这样的数据分组操作非常实用。

>>> 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'}, {'a
ddress'
: '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/201
2'
},
... ]
>>> from operator import itemgetter
>>> from itertools import groupby
>>>
>>> rows.sort(key=itemgetter('date'))
>>>
>>> for date, items in groupby(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'}

groupby()函数扫 整个序列并且查找连续相同值(或者根据指定key函数返回值相同)的元 素序列。 在每次迭代的时候,它会返回一个值和一个迭代器对象, 这个迭代器对象可以 生成元素值全部等于上面那个值的组中所有对象。

一个非常重要的准备步骤是要根据指定的字段将数据排序。 因为groupby()仅仅检查连 续的元素,如果事先并没有排序完成的话,分组函数将得不到想要的结果。

如果你仅仅只是想根据date字段将数据分组到一个大的数据结构中去,并且允许随机访 问, 那么你最好使用 defaultdict()来构建一个多值字典:

from collections import defaultdict rows_by_date = defaultdict(list) for row in rows:
rows_by_date[row['date']].append(row)
>>> for r in rows_by_date['07/01/2012']:
... print(r)
...
{'date': '07/01/2012', 'address': '5412 N CLARK'} {'date': '07/01/2012', 'address': '4801 N BROADWAY'} >>>

过滤序列元素

问题:
你有一个数据序列,想利用一些规则从中 取出需要的值或者是缩短序列。

解决方案:
最简单的过滤序列元素的方法就是使用列表推导。比如:

>>> mylist = [1, 4, -5, 10, -7, 2, 3, -1] >>> [n for n in mylist if n > 0]
[1, 4, 10, 2, 3]
>>> [n for n in mylist if n < 0]
[-5, -7, -1] >>>

使用列表推导的一个潜在缺陷就是如果输入非常大的时候会产生一个非常大的结果集,占 用大量内存。 如果你对内存比较敏感,那么你可以使用生成器表达式迭代产生过滤的元 素:

>>> pos = (n for n in mylist if n > 0)
>>> pos
<generator object <genexpr> at 0x1006a0eb0> >>> for x in pos:
... print(x) ...
1
4
10 2
3 >>>

有时候,如果过滤规则比较复杂,不能简单地在列表推倒或者生成器表达式中表达出来,你可以将源代码放到一个函数中,然后使用内建的filter()函数:

>>> values = ['1', '2', '-3', '-', '4', 'N/A', '5']
>>> def is_int(val):
... try:
... x = int(val)
... return True
... except ValueError:
... return False
...
>>> ivals = list(filter(is_int, values))
>>> ivals
['1', '2', '-3', '4', '5']

filter()函数创建了一个迭代器,因此如果你想得到一个列表的话,就像实例那样去使用list()去转换。

列表推导和生成器表达式通常情况下是过滤数据最简单的方式。其实它们还能在过滤的 时候转换数据:

过滤的同时开平方:

>>> mylist = [1, 4, -5, 10, -7, 2, 3, -1]
>>> import math
>>> [math.sqrt(n) for n in mylist if n > 0]
[1.0, 2.0, 3.1622776601683795, 1.4142135623730951, 1.7320508075688772]

过滤操作的一个变种就是将不符合条件的值用新的值代替,而不是丢弃它们:

#将小于0的数用0替代
>>> clip_neg = [n if n > 0 else 0 for n in mylist]
>>> clip_neg
[1, 4, 0, 10, 0, 2, 3, 0]

#将大于0的数用0替代
>>> clip_pos = [n if n < 0 else 0 for n in mylist]
>>> clip_pos
[0, 0, -5, 0, -7, 0, 0, -1]
>>>

另外一个值得关注的过滤工具就是itertools.compress(),它以一个可迭代对象和一个相对应的布尔选择器序列作为输入参数,输出可迭代对象中对应选择器为True的元素。当你需要用另一个相关联的序列来过滤某个序列的时候,这个函数是非常有用的。如:

假如你现在有如下两组数据:

addresses = [
'5412 N CLARK',
'5148 N CLARK', '5800 E 58TH',
'2122 N CLARK'
'5645 N RAVENSWOOD', '1060 W ADDISON', '4801 N BROADWAY', '1039 W GRANVILLE',
]
counts = [ 0, 3, 10, 4, 1, 7, 6, 1]

现在你想将那些对应count值大于5的地址全部输出,那么你可以这样做:

>>> from itertools import compress
>>> more5 = [n > 5 for n in counts]
>>> more5
[False, False, True, False, False, True, True, False] >>> list(compress(addresses, more5))
['5800 E 58TH', '4801 N BROADWAY', '1039 W GRANVILLE'] >>>

这里的关键点在于先创建一个布尔序列,指示哪些元素符合条件,然后compress()函数根据这个序列去选择输出对应位置为True的元素。
和filter()函数类似,compress()函数也是返回一个迭代器。因此如果你需要得到一个列表,那么你需要使用list()来将结果转换为列表类型。

从字典中 取子集

问题:
你想构造一个字典,它是另外一个字典的子集。

解决方案:

最简单的方式是使用字典推导。如:

prices = {
'ACME': 45.23,
'AAPL': 612.78, 'IBM': 205.55, 'HPQ': 37.20, 'FB': 10.75
}
# Make a dictionary of all prices over 200
p1 = {key: value for key, value in prices.items() if value > 200}
# Make a dictionary of tech stocks
tech_names = {'AAPL', 'IBM', 'HPQ', 'MSFT'}
p2 = {key: value for key, value in prices.items() if key in tech_names}

大多数情况下,字典推导能做到的,通过创建一个元组序列然后把它传给dict()函数也能实现:

p1 = dict((key, value) for key, value in prices.items() if value > 200)

但是字典推导方式表意更清晰,并且实际上会运行地更快些。

第二个例子也可以这样写:

# Make a dictionary of tech stocks
tech_names = { 'AAPL', 'IBM', 'HPQ', 'MSFT' }
p2 = { key:prices[key] for key in prices.keys() & tech_names }

但是运行时间测试结果显示这种方案大概比第一种方案慢1.6倍。