建议大家先自己做,然后在看《代码》部分的答案
文章目录
数据结构和算法
python内置的数据结构有list,set以及dic等,大多数情况,我们可以直接使用这些数据结构,但是,通常我们也需要考虑搜索、排序、排列以及筛选等问题。因此,本章的目的就是来探讨常见的数据结构和通数据有关的算法。
将序列分解为单独的变量
问题
有一个包含N个元素的元组或序列,现在将它分解为N个单独的变量
解决方案
lis = [1, 2, 3]
x, y, z = lis
讨论
实际上不仅是list,tuple等只要是对象且可迭代的,那么就可以将执行分解操作(字符串、文件、迭代器以及生成器)
# tuple
a, b, c = (1, 2, 3)
# 字符串
a, b, c = 'abc'
# list生成器
a, b, c = [x for x in range(3)]
从任意长度的可迭代对象中分解元素
问题
需要从某个可迭代对象中分解出N个元素,但是这个迭代对象的长度可能超过N,这会导致出现“分解过多”的异常
解决方案
python的“*表达式”可以解决这个问题,
场景假设1:假设开设一门课程,并决定在期末的作业成绩中去掉第一个和最后一个,只对中间的成绩做平均分统计,但是,每个班人数有区别。
def drop(gread_list):
a, *middle, b = gread_list
# return avg(middle)
return middle / len(middle)
场景假设2:假设有一些用户记录,记录由姓名和电子邮件地址组成,后面跟着任意数量的电话号码,获取这些号码
inform = {"Dave", "dave@q.com", "1233445", "11334455"}
name, ema, *num = inform
由*修饰的变量也可以位于列表的第一位
场景假设3:假设用一系列的值来代表公司过去8个季度的销售额。如果想对最近的一个季度的销售额同前7个季度的平均值作比较 。
def demo(list):
*trailing, current = list
avg = trailing / len(trailing)
return current/avg
讨论
对于分解为止或任意长度的可迭代对象,可利用*表达式分解可迭代对象使得开发者能够利用这些对象,可以不必做复杂操作即可获得相关的元素。在实际编程中该表达式在迭代一个变长的元组序列时尤其有用。
rocords = [
('foo', 1, 2),
('bar', 'hellow'),
('foo', 3, 4),
]
def do_foo(x, y):
print('foo', x, y)
def do_bar(s):
print('foo', s)
for tag, *arg in rocords:
if tag == 'foo':
do_foo(*arg)
elif tag == 'bar':
do_bar(*arg)
当和某些特定的字符串处理操作相结合,比如splitting时用*表达式
line = '12:324:435:654:325'
a, *b, c = line.split(':')
print(*b) # 324 435 654
有时候可能想分解出某些值然后丢弃,在分解的时候,不能只是指定一个单独*,但是可以使用几个常用来表示待丢弃值的变量名,比如_或者ign(ingored)。
record = ('acme', 50, 132, (12, 13, 14321))
a, *_, (*_, year) = record
*分解操作和各种函数式语言中的列表处理功能有着一定的相似性。例如,如果有一个列表,可以像下面这样轻松将其分解为头部和尾部。
items = [1, 2, 3, 4, 5, 6]
head, *mean = items
在编写执行这类拆分功能的函数时,人们可以假设这是为了实现某种递归的算法
def sum(item):
head, *a = item
return head + sum(a)
保存最后N个元素
问题
我们希望在迭代或是其他形式的处理过程中对最后几项纪录做一个有限的历史记录统计。
解决方案
保存有限的历史记录是collections.deque很好地应用场景。
例如在对于一系列文本行做简单的文本匹配操作,当发现有匹配时就输出当前的匹配行以及最后检查过的N行文本。
from collections import deque
def search(lines, pattern, history=5):
previous_lines = deque(maxlen=history)
for line in lines:
if pattern in line:
yield line, previous_lines
previous_lines.append(line)
if __name__ == '__mian__':
with open('') as f:
for line, prevlines in search(f, 'python', 5):
for pline in prevlines:
print(pline, end='')
print(line, end='')
print('-' * 20)
知识点补充1:deque是python标准库collections中的一个类,实现了两端都可以操作的队列,相当于两端队列与,与python的基本数据类型列表很相似。
知识点补充2:带yield的函数是一个迭代器,在函数内部碰到yield 的时候,函数会返回某个值,并停留在这个位置,当下次执行函数后,会在上次停留的位置继续运行。
讨论
在上面例子中,当编写搜索某项纪录的代码时,通常会用到含有yield的生成函数,deque(maxlen=N)创建了一个固定长度的队列。当有新纪录加入而队列已满时会自动移除最老的那条记录。
from collections import deque
q = deque(maxlen=1)
q.append(1)
q.append(2)
q.append(3)
print(q)
尽管可以在列表上手动完成这样的操作,但是队列这种解决方案要更加优雅,更普遍的是当需要一个简单的队列结构时,deque可以事半功倍。如果不指定队列的大小,那么就可以获得一个无限的队列,可以再两端执行添加和弹出操作
from collections import deque
q = deque()
for i in range(4):
q.append(i)
q.popleft()
q.pop()
q.appendleft(1)
print(q)
找到最大或者最小的N个元素
问题
我们想在某个集合中找到最大或者最小的N个元素
解决方案
heapq模块中有两个函数——nlargest()和nsmallest()可以很好地解决问题
import heapq
list = []
for i in range(10):
list.append(i)
a = heapq.nlargest(2, list)
b = heapq.nsmallest(2, list)
print(a, b)
这两个函数都可以接受一个参数Key,从而允许他们工作的更加复杂的数据结构之上。
import heapq
list = [
{'name': 'a', "share": 1, "peice": 12},
{'name': 'b', "share": 2, "peice": 13},
{'name': 'c', "share": 3, "peice": 14},
{'name': 'd', "share": 4, "peice": 15},
{'name': 'e', "share": 5, "peice": 16},
]
# 找到价格最低的商品信息
cheap = heapq.nsmallest(1, list, key=lambda s: s['peice'])
print(cheap)
expensive = heapq.nlargest(1, list, key=lambda s: s['peice'])
print(expensive)
讨论
如果正在寻找最大或最小的N个元素,且同集合中元素的综述相比,N很小,那么下面这些函数可以提供很好地性能。这些函数首先会在低层将数据转换为列表,且元素会以堆的顺序排列
import heapq
num = []
for i in range(7):
num.append(i)
heap = heapq.heapify(num) # 将列表转换为堆
print(heap)
堆最重要的特性就是heap[0]总是最小的那个元素。此外,接下来的元素可一次通过heapq.headppop()方法轻松找到,该方法会将第一个元素(最小的)弹出,然后以第二小的元素取而代之,当所要找的元素数量相对较小时,函数nlargest和nsmallest才是最适合的。如果只是简单的想找到最小火最大的元素,那么min()max()会更加快。
实现优先级队列
问题
我们想要实现一个队列,他能够用以给定的优先级来对元素排序,且每次pop操作都会返回优先级最高的元素。
解决方案
import heapq
class PriorityQueue:
def __init__(self):
self._queue = []
self._index = 0
def push(self, item, priority):
heapq.heappush(self._queue, (-priority, self._index, item))
self._index += 1
def pop(self):
return heapq.heappop(self._queue)[-1]
class item:
def __init__(self, name):
self.name = name
def __repr__(self):
return 'Item({!r})'.format(self.name)
q = PriorityQueue()
q.push(item('foo'), 1)
q.push(item('bar'), 5)
q.push(item('spam'), 1)
q.push(item('grok'), 3)
print(q.pop())
print(q.pop())
print(q.pop())
print(q.pop())
代码中第一次执行pop()操作时返回的元素具有最高的优先级。我们也观察到拥有相同优先级的两个元素(foo和grok)返回的顺序同他们插入到队列时的顺序相同。
知识点补充1:def init(self):在python里面很常见,是用于初始化类。
①self.valueName
valueName:表示self对象,即实例的变量。与其他的,Class的变量,全局的变量,局部的变量,是相对应的。
②self.function()
function:表示是调用的是self对象,即实例的函数。与其他的全局的函数,是相对应的
③self的作用:在class中,需要访问当前的实例中的变量和函数时(instance),函数的第一个参数,就必须是实例对象本身,并且建议,约定俗成,把其名字写为self,以self为前缀的变量都可供类中的所有方法使用
知识点补充2:def repr() 主要实现 “自我描述” 功能——当直接打印类的实例化对象时,系统将会自动调用该方法,输出对象的自我描述信息,用来告诉外界对象具有的状态信息。但是,object 类提供的 repr() 方法总是返回一个对象(类名 + obejct at + 内存地址),这个值并不能真正实现自我描述的功能!因此,如果你想在自定义类中实现 “自我描述” 的功能,那么必须重写 repr 方法。
讨论
在上面代码中,队列以元组的形式组成。把priority取负值是为了让队列能够按元素的优先级从高到低的顺序排列,这和正常的堆排列顺序相反,一般情况下堆是按从小到大的顺序排列的。变量index的作用是为了将具有相同优先级的元素以适当的顺序排列。通过维护一个不断增加的索引,元素将以他们入队列时的顺序来排列。但是,index在对具有相同有限级的元素间做比较操作时同样扮演了重要的角色。如果以元组的形式来表示元素,那么只要优先级不同,他们可以进行比较。但是,如果两个元组的优先级值相同,做比较操作时还是会像之前那样失败。
通过引入额外的索引值,以(priority,index,item)的方式建立元组,就可以完全避开这个问题,因为没有哪两个元组会有相同的index值(一旦比较操作的结果可以确定,python就不会再去比较剩下的元组元素了)。
如果想用这个队列用于线程间的通讯,还需要增加适当的锁和信号限制。
在字典中将键映射到多个值上
问题
我们想要一个能将key映射到多个值得字典(multidict)
解决方案
字典是一种关联容器,每个键都映射到一个单独的值上,如果想让键映射到多个值,需要将多个值保存到一个容器如列表或者集合中,如果要创建如下字典
d = {
'a': [1, 2, 3],
'b': [4, 5]
}
e = {
'a': {1, 2, 3},
'b': {4, 5}
}
要使用列表还是集合取决于应用的意图。如果希望保留元素插入的顺序,就用列表。如果希望消除重复元素(并且不在意他的顺序),就用集合。为了能方便的创建这样的字典,可以利用collections模块中的defaultdict类。defaultdict的一个特点就是可以自动初始化第一个值,这样只需关注添加元素即可。
from collections import defaultdict
# 创建由列表构成的字典
d =defaultdict(list)
d['a'].append(1)
d['a'].append(1)
d['b'].append(3)
print(d)
# 创建由集合构成的字典
f = defaultdict(set)
f['a'].add(1)
f['a'].add(1)
f['b'].add(1)
print(f)
关于defaultdict,需要注意的一个地方是,它会自动创建字典表项以待稍后的访问(即使这些表项当前在字典中还没有找到)。如果不想要这个功能,可以在普通的字典上调用setdefault()方法来取代。
解决方案
原则上,构建一个一键多值字典是很容易的。但是如果试着自己对第一个值做初始化操作,这就会变得很混乱。
让字典保持有序
问题
我们想要创建一个字典,同时当对字典做迭代或序列化操作时,也能控制其中元素的序列。
解决方案
要控制字典中元素的顺序,可以使用collections模块中的orderedDict类。当对字典做迭代时,他会严格按照元素初始添加的顺序进行。
from collections import OrderedDict
d = OrderedDict()
d['foo'] = 1
d['bar'] = 2
d['spam'] = 3
d['grok'] = 4
for key in d:
print(key, d[key])
当想构建一个映射结构以便稍后对其做序列化或编码成另一种格式时,OderedDict就显得特别有用。例如,如果想在进行JSON编码时精确控制各字段的顺序,那么只要首先在OrederedDict中构建数据就可以了。
import json
from collections import OrderedDict
d = OrderedDict()
d['foo'] = 1
d['bar'] = 2
d['spam'] = 3
d['grok'] = 4
json.dumps(d)
讨论
OrderedDict内部维护了一个双向链表,他会根据元素加入的顺序来排列键的位置。第一个新加入的元素被放在链表的末尾。接下来对已存在的链做重新赋值不会改变键的顺序。请注意OrderedDict的大小是普通字典的2倍多,这是由于它额外创建的链表所致。因此,如果打算构建一个涉及大量OrderedDict实例的数据结构(例如CSV文件中读取1**4行内容到OrderedDict列表中),那么需要认真对应应用做需求分析,从而判断使用OrderedDict所带来的好处是否能超越因额外的内存开销所带来的缺点。
与字典有关的计算问题
问题
我们想在字典对数据执行各式各样的计算(比如求最值、排序等)。
解决方案
假设有一个字典在股票名称和对应的价格间做了一个映射:
prices = {
'ACME': 45, 'ACME1': 46, 'ACME2': 47, 'ACME3': 55
}
为了能对字典内容做些有用的计算,通常会利用zip()将字典的键和值翻转过来。例如,下面的代码会告诉我们如何找出价格最低和最高的股票。
prices = {
'ACME': 45, 'ACME1': 46, 'ACME2': 47, 'ACME3': 55
}
# zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。
min_price = min(zip(prices.values(), prices.keys()))
max_price = max(zip(prices.values(), prices.keys()))
prices_sort = sorted(zip(prices.values(), prices.keys()))
# 计算时需要注意,zip()创建了一个迭代器,它的内容只能被消费一次
讨论
如果尝试在字典上执行常见的数据操作,将会发现他们只会处理键,而不是值,这可能不是我们所期望的,因此实际上我们是尝试对字典的值做计算。可以利用字典的values()方法来解决问题。不幸的是,通常这也不是我们所期望的。比如,我们可能想知道相应的键所关联的信息是什么,如果提供一个key参数传递给min()、max()就能得到最大值和最小值所对应的键是什么,但是要得到最小值的话,还需要额外执行一次查找。
prices = {
'ACME': 45, 'ACME1': 46, 'ACME2': 47, 'ACME3': 55
}
a = min(prices, key=lambda k: prices[k])
b = max(prices, key=lambda k: prices[k])
c = prices[min(prices, key=lambda k: prices[k])]
print(a, b, c)
利用了zip()的解决方案使用过将字典的键值对翻转来实现的,当在这样的元组上执行比较操作时,值会先进行比较,然后才是键。这完全符合我们的期望,允许我们呢用一条单独的语句轻松地对字典里的内容作整理和排序。
应该要注意的是,当设计(value,key)对的比较时,如果碰巧有多个条目拥有相同的value值,那么此时key将用来作为判定结果的依据。例如,在计算最值时,如果出现重复值,则将返回拥有最值key值得那个条目。
prices = {
'ACME': 45, 'ACME1': 45, 'ACME2': 45, 'ACME3': 45
}
a = min(zip(prices.values(), prices.keys()))
print(a)
在两个字典中寻找相同点
问题
在两个字典,我们想找出他们之间可能相同的地方(相同的键、相同的值等)。
解决方案
a = {
'x': 1,
'y': 2,
'z': 3
}
b = {
'w': 10,
'x': 11,
'y': 2
}
print(a.keys() & b.keys())
print(a.keys() - b.keys())
print(a.items() & b.items())
c = {key: a[key] for key in a.keys() - {'z', 'w'}}
print(c)
讨论
字典就是一系列键和值之间的映射集合。字典的keys()方法会返回keys-view对象,其中暴露了所有的key。关于字典的键有一个很少有人知道的特性,那就是他们也支持常见的集合操作,比如求交并等。因此,如果需要对字典的键做常见的集合操作,那么就能直接使用key-view对象而不必先将它们转化为集合。
字典的items()方法返回由(key,value)对组成的items-view对象。这个对象支持类似的集合操作,可用来完成找出两个字典间有哪些键值对有相同之处的操作。
尽管类似,但字典的values()方法并不支持集合操作。部分原因是因为在字典中键和值是不同的,从值得角度来看并不能保证所有值都是唯一的。单这一条原因就使得某些特定的集合操作是有问题的。但是必须执行这样的操作,还是可以先将值转化为集合来实现。
从序列中移除重复项且保持元素间顺序不变
问题
我们想去除序列中出现的重复元素,但仍然保持剩下的元素顺序不变。
解决方案
如果序列中的值是可哈希的,那么这个问题可以通过使用集合和生成器轻松解决。
def dedupe(items):
seen = set()
for item in items:
if item not in seen:
yield item
seen.add(item)
a = [1, 2, 3, 4, 1, 2, 3, 4]
print(list(dedupe(a)))
带yield的函数是一个迭代器,在函数内部碰到yield的时候,函数会返回某个值,并停留在这个位置,当下次执行函数后,会在上次停留的位置继续执行。只有当序列中的元素是可哈希的时候才能这么做。如果想在不可哈希的对象(列表)序列中去除重复项,需要对上述代码稍作修改。
def dedupe(items, key=None):
seen = set()
for item in items:
val = item if key is None else key(item)
if val not in seen:
yield item
seen.add(val)
a = [{'x': 1, "y": 2}, {'x': 3, "y": 4}, {'x': 5, "y": 6}, {'x': 7, "y": 8}]
print(list(dedupe(a, key=lambda d: (d['x'], d['y']))))
讨论
如果想要做的知识去除重复项,那么通常足够简单的办法就是构建一个集合。但是这种方法不能保证元素的顺序不变,因此得到的结果会被打乱。前面展示的解决方案可避免出现这个问题。
找出序列中出现次数最多的元素
问题
我们有一个元素序列,想知道在序列中出现次数最多的元素是什么。
解决方案
collections模块中的Counter类正是为此类问题所设计的。它甚至有一个非常方便的most_common()方法可以直接告诉我们答案。为了说明用法,假设有一个列表,列表中是一系列的单词,我们想要找到那些单词出现的最为频繁。
from collections import Counter
words = []
word_couts = Counter(words)
top_three = word_couts.most_common(3)
print(top_three)
讨论
可以给Counter对象提供任何可哈希的对象序列作为输入。在底层实现中,Counter是一个字典,在元素和它们出现的次数间做了映射。
通过公共键对字典列表排序
问题
我们有一个字典列表,想要根据一个或多个字典中的值来对列表排序。
解决方案
利用operator模块中的itemgetter函数对这类结构进行排序是非常简单的。假设通过查询数据库表项获取网站上的成员列表,我们得到如下的结构。
from operator import itemgetter
rows = [
{'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
{'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
{'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]
rows_by_fname = sorted(rows, key=itemgetter('fname'))
rows_by_uid = sorted(rows, key=itemgetter('uid'))
rows_by_lfname = sorted(rows, key=itemgetter('lname', 'fname'))
print(rows_by_fname)
print(rows_by_uid)
print(rows_by_lfname)
讨论
在这个例子中,rows被传递给内建的sorted()函数,该函数接受一个关键字参数key。这个参数应该代表一个可调用对象(callable),该对象从rows中接受一个单独的元素作为输入并返回一个用来做排序依据的值。itemgetter()函数创建的就是这样一个可调用对象。函数operator.itemgetter()接受的参数可作为查询的标记,用来从rows的记录中提取出所需的值。他可以是字典的键名称、用数字表示的列表元素或是任何可以传给对象的__getitem__()方法的值。如果传多个标记给itemgetter(),那么它产生的可调用对象将返回一个包含所有元素在内的元组,然后,sorted()将根据对元组的排列结果来排列输出结果。如果相同时针对多个字段做排序,那么这是非常有用的。
对不原生支持比较操作的对象排序
问题
我们想在同一类的实例之间做排序,但是它们并不原生支持比较操作。
解决方案
内建的sorted()函数可接受一个用来传递可调用对象(callable)的参数key,而该可调用对象会返回待排序对象中的某些值,sorted则利用某些值来比较对象。例如,如果应用中有一系列的User对象实例,而我们想通过Use_id属性来对它们排序,则可以提供一个可调用对象将User实例作为输入然后user_id。
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)]
print(users)
print(sorted(users, key=lambda u: u.user_id))
讨论
lambda表达式可理解为匿名函数,即有时在使用函数时不需要给函数分配一个名称,该函数就是匿名函数。
语法:lambda 参数列表:lambda体
lambda是关键字声明,在lambda表达式中,参数列表与函数中的参数列表一样,但不需要用小括号括起来,默哀好后面是lambda体,lambda表达式的主要代码在lambda体处编写,类似于函数体。
注意:lambda体不能是一个代码块,不能包含多条语句,只能包含一条语句,该语句会计算一个结果返回给lambda表达式,但与函数不同的是,不需要使用return语句返回,而且当使用函数作为参数的时候。lambda表达式非常有用,可以简化代码。
lambda函数与def函数的区别在于:(1)lambda可以立即传递,自行返回结果(2)lambda在内部只能包含一行代码(3)lambda是一个为编写简单函数而设计的,而def用来处理更大的任务(4)lambda可以定义一个匿名函数,而def定义的函数必须有一个名字。
import random
# 把lambda当做函数使用
f = lambda a, b: a + b
print(f(1, 2))
print("----------------------------")
# lambda定义带有默认值的函数
f1 = lambda a, b=1, c=1: a + b + c
print(f1(1))
print(f1(1, b=2, c=2))
print("----------------------------")
# lambda填充list的值
f2 = [(lambda x: x ** 1), (lambda x: x ** 2), (lambda x: x ** 3)]
print(f2[0](2), f2[1](2), f2[2](2))
print("----------------------------")
# lambda填充字典的值
f3 = {'f1': (lambda: 2 + 3), 'f2': (lambda: 2 * 3), 'f3': (lambda: 2 ** 3)}
print(f3['f1'](), f3['f2'](), f3['f3']())
print("----------------------------")
# 使用lambda清洗列表
data = list(range(20))
print(data)
random.shuffle(data)
print(data)
data.sort(key=lambda x: len(str(x)))
print(data)
print("----------------------------")
根据字段将记录分组
问题
有一系列的字典或对象实例,我们想根据某个特定的字段(比如日期)来分组迭代数据。
解决方案
itertools.groupby()函数在对数据进行分组时特别有用。
from operator import itemgetter
from itertools import groupby
rows = []
rows.sort(key=itemgetter('date'))
for data, items in groupby(rows, key=itemgetter('data')):
print(data)
for i in items:
print(' ', i)
讨论
函数groupbu()通过扫描序列找出拥有相同值(或是由参数key指定的函数返回的值)的序列项,并将他们分组。groupby()创建了一个迭代器,而在每次迭代时都会返回一个值(value)和一个子迭代器(sub_iterator),这个子迭代器可以产生所有在该分组内具有该值的项。在这里重要的是首先根据感兴趣的字段对数据进行排序,因为groupby()只能检查连续的项,不首先排序的话,将无法按所想的方式来对记录分组。如果只是简单的根据日期将数据分组到一起,放进一个大的数据结构中以允许进行随机访问,那么利用defaultdict()构建一个一键多值字典可能会更好