写在前面
- 给小伙伴推荐
Python
书单,发现了这本书 - 开卷获益,有一种呼之欲出想全部读完的感受
- 这么好的书在身边,居然都没看过,懊悔不已....
- 遂连夜拜读,受益匪浅,这里整理笔记
「 我的手机里,最初是有网抑云
的,上学时,不开心,会听应景的歌,偶尔看评论,虽不会唱,有种被感同身受。后来,手机存储不够,清理,提示卸载不常用的软件就卸载了,恍惚,好久不听歌了,想起在哪看到,有些人二十岁就死了,等到八十岁才被埋
。------山河已无恙」
第一章 数据结构和算法
Python内置了许多非常有用的数据结构
,比如列表(list)、集合(set)
以及字典(dictionary)、元组(tuple)
。在collections
模块中也包含了针对各种数据结构的解决方案。
将序列分解为单独的变量
「我们有一个包含N个元素的元组或序列,现在想将它分解为N个单独的变量。」
代码语言:javascript复制┌──(liruilong㉿Liruilong)-[/mnt/e/docker]
└─$ python3
Python 3.9.10 (main, Jan 16 2022, 17:12:18)
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> p = (4,5)
>>> x,y = p
>>> x
4
>>> y
5
>>>
居然可以这样,长见识了,类似于JavaScript ES6
中的解构赋值
,如果当成函数对象来看,可以看做是拆包
>>> data = [ 'ACME',50,91.1,(2012,12,21)]
>>> n,s,p,d = data
>>> n,s
('ACME', 50)
>>> d
(2012, 12, 21)
>>>
当然,也支持深层次解构
代码语言:javascript复制>>> data = [ 'ACME',50,91.1,(2012,12,21)]
>>> name,shares,price,(year,mon,day)=data
>>> year
2012
>>> mon,day
(12, 21)
>>>
如果元素的数量不匹配
,将得到一个错误提示。
>>> p = (4,5)
>>> x,y,z = p
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 3, got 2)
>>>
实际上不仅仅只是元组或列表
,只要对象恰好是可迭代
的,那么就可以执行分解操作
。这包括字符串、文件、迭代器
以及生成器
。
>>> s = 'love'
>>> a,b,c,d = s
>>> a,b
('l', 'o')
>>> d
'e'
>>>
当做分解操作
时,想丢弃某些特定的值。可以用 '_'充当占位符,,这个在JavaScript ES6
和Golang
也都支持。
>>> data = [ 'ACME',50,91.1,(2012,12,21)]
>>> _,_,a,_=data
>>> a
91.1
>>>
从任意长度的可迭代对象中分解元素
「需要从某个可选代对象
中分解出N个元素
,但是这个可迭代对象的长度可能超过N,这会导致出现分解的值过多(too many values to unpack)
的异常。」
>>> record=('Dave','davedexample.com','773-555-1212','1847-555-1212')
>>> name,email,*phone_numbers = record
>>> name
'Dave'
>>> phone_numbers
['773-555-1212', '1847-555-1212']
>>>
类似于Java方法传值的可变参数
一样,但是要比java高级的多
,java的可变参数只能最后一个,python 则可以在任意位置
>>> record=('Dave','davedexample.com','1224965096@qq.com','773-555-1212')
>>> name,*email,phone = record
>>> name
'Dave'
>>> email
['davedexample.com', '1224965096@qq.com']
>>>
* 式的语法在选代一个变长的元组序列时尤其有用
records = [
('foo', 1, 2),
('bar', 'hello'),
('foo', 3, 4),
]
def do_foo(x, y):
print('foo', x, y)
def do_bar(s):
print('bar', s)
for tag, *args in records:
if tag == 'foo':
do_foo(*args)
elif tag == 'bar':
do_bar(*args)
字符串拆分是的使用 : ''.split('')
>>> line='nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false'
>>> uname,*fields,homedir,sh=line.split(':')
>>> uname
'nobody'
>>> sh
'/usr/bin/false'
>>> fields
['*', '-2', '-2', 'Unprivileged User']
>>>
也可 '*' 和 '_'
两个连用,丢弃多个变量
>>> uname,*_,homedir,sh=line.split(':')
>>> sh
'/usr/bin/false'
>>>
求和递归
代码语言:javascript复制items=[1,10,7,4,5,9]
def sum(items):
head,*tail=items
return head sum(tail) if tail else head
保存最后N个元素(队列)
「我们希望在迭代或是其他形式的处理过程中对最后几项记录做一个有限的历史记录统计。」
保存有限的历史记录可算是collections.deque
的完美应用场景了
打印满足条件的最后5条记录
代码语言:javascript复制#deque(maxlen=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)
# Example use on a file
if __name__ == '__main__':
with open('somefile.txt') as f:
for line, prevlines in search(f,'python',5):
for pline in prevlines:
print(pline, end='')
print(line, end='')
print('-1'*20)
deque(maxlen=N)
创建了一个固定长度的双端队列
>>> from collections import deque
>>> q = deque()
>>> q.append(1)
>>> q.append(2)
>>> q.append(3)
>>> q
deque([1, 2, 3])
>>>
尽管可以在列表上手动完成这样的操作(append、del),但队列这种解决方案要优雅得多,运行速度也快得多。从队列两端添加或弹出元素的复杂度都是O(1)
。这和列表不同,当从列表的头部插入或移除元素时,列表的复杂度为O(N)
找到最大或最小的N个元素
「我们想在某个集合中找出最大或最小的N个元素。」
heapq
模块中有两个函数nlargest()和nsmallest()
,当所要找的元素数量相对较小
时,函数nlargest()和nsmallest()
才是最适用的,nlargest()和nsmallest()
的实际实现会根据使用它们的方式而有所不同,可能会相应作出一些优化措施(比如,当N的大小同输入大小很接近
时,就会采用排序
的方法)。
>>> import heapq
>>> nums=[1,8,2,23,7,-4,18,23,42,37,2]
>>> heapq.nlargest(3,nums)
[42, 37, 23]
>>> heapq.nsmallest(3,nums)
[-4, 1, 2]
>>>
这两个函数都可以接受一个参数key,从而允许它们工作在更加复杂的数据结构之上
代码语言:javascript复制import heapq
portfoli = [
{'name': 'AAPL', 'shares': 50, 'price': 543.22},
{'name': 'FB', 'shares': 200, 'price': 21.09},
{'name': 'HPQ', 'shares': 35, 'price': 31.75},
{'name': 'YHOO', 'shares': 45, 'price': 16.35},
{'name': 'ACME', 'shares': 75, 'price': 115.65}
]
print(heapq.nsmallest(2,portfoli,key=lambda s: s['price']))
print(heapq.nlargest(2,portfoli,key=lambda s: s['shares']))
代码语言:javascript复制[{'name': 'YHOO', 'shares': 45, 'price': 16.35}, {'name': 'FB', 'shares': 200, 'price': 21.09}]
[{'name': 'FB', 'shares': 200, 'price': 21.09}, {'name': 'ACME', 'shares': 75, 'price': 115.65}]
如果正在寻找最大或最小的N个元素,且同集合中元素的总数目相比,N很小
,那么heapq.heapify(heap)
函数可以提供更好的性能。这些函数首先会在底层将数据转化成列表,且元素会以堆的顺序排列
。
>>> nums
[1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
>>> heap=list(nums)
>>> heapq.heapify(heap)
>>> heap
[-4, 2, 1, 23, 7, 2, 18, 23, 42, 37, 8]
>>> heapq.heappop(heap)
-4
>>> heapq.heappop(heap)
1
>>> heapq.heappop(heap)
2
>>>
堆最重要的特性就是heap[0]总是最小那个的元素
。此外,接下来的元素可依次通过heapq.heappop
方法轻松找到。该方法会将第一个元素(最小的)弹出,然后以第二小的元素取而代之(这个操作的复杂度是O(logN),N代表堆的大小)
想找到最小或最大的元素(N=1时)
,那么用min()和max)会更加快。
>>> min(heap)
2
>>> max(heap)
42
>>> heap
[2, 7, 8, 23, 42, 37, 18, 23]
>>>
如果N和集合本身的大小差不多大
,通常更快的方法是先对集合排序,然后做切片操作
,使用sorted(items)[:N]或者sorted(items)[-N:])
。
实现优先级队列
「我们想要实现一个队列,它能够以给定的优先级来对元素排序,且每次pop操作时都会返回优先级最高的那个元素。」
heapq模块提供了堆排序算法的实现,heapq有两种方式创建堆,
- 一种是使用一个空列表,然后使用heapq.heappush()函数把值加入堆中
- 一种就是使用heap.heapify(list)转换列表成为堆结构
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):
#!r直接反应对象本体
return 'Item({!r})'.format(self.name)
q = PriorityQueue()
q.push(Item('foo'),1)
q.push(Item('bar'),5)
q.push(Item('spam'),4)
q.push(Item('grok'),1)
print(q.pop())
print(q.pop())
print(q.pop())
队列以元组(-priority,index,item)的形式组成
。把priority取负值
是为了让队列能够按元素的优先级从高到低的顺序排列
。一般情况下是最小堆。
变量index
的作用是为了将具有相同优先级
的元素以适当的顺序排列。通过维护一个不断递增的索引
,元素将以它们入队列时的顺序来排列
。没有哪两个元组会有相同的index值
(一旦比较操作的结果可以确定,Python就不会再去比较剩下的元组元素了)
如果想将这个队列用于线程间通信,还需要增加适当的锁和信号机制
在字典中将键映射到多个值上
「我们想要一个能将键(key)映射到多个值的字典(即所谓的一键多值字典[multidict])」
字典是一种关联容器,每个键都映射到一个单独的值上。如果想让键映射到多个值,需要将这多个值保存到另一个容器如列表或集合中。
为了能方便地创建这样的字典,可以利用collections模块
中的defaultdict类
。defaultdict
的一个特点就是它会自动初始化第一个值
,这样只需关注添加元素
即可。
defaultdict(<class 'list'>, {'a': [1, 2], 'b': [4]})
>>> from collections import defaultdict
>>> d = defaultdict(list)
>>> d['a'].append(1)
>>> d['a'].append(2)
>>> d['b'].append(4)
>>> d
defaultdict(<class 'list'>, {'a': [1, 2], 'b': [4]})
代码语言:javascript复制>>> d= defaultdict(set)
>>> d['a'].add(1)
>>> d['a'].add(3)
>>> d['a'].add(3)
>>> d
defaultdict(<class 'set'>, {'a': {1, 3}})
>>>
用于分组,有一种java8里面Stream的味道
代码语言:javascript复制d=defaultdict(list)
for key, value in pairs:
d[key]. append(value)
让字典保持有序
要控制字典中元素的顺序,可以使用collections模块中的OrderedDict
类。当对字典做迭代时,它会严格按照元素初始添加的顺序进行。例如:
>>> from collections import OrderedDict
>>> d = OrderedDict()
>>> d['1']=1
>>> d['2']=2
>>> d['3']=3
>>> d
OrderedDict([('1', 1), ('2', 2), ('3', 3)])
>>>
当想构建一个映射结构以便稍后对其做序列化或编码成另一种格式时,OrderedDict
就显得特别有用。例如,如果想在进行JSON编码时精确控制各字段的顺序,那么只要首先在OrderedDict中构建数据就可以了。
>>> import json
>>> json.dumps(d)
'{"1": 1, "2": 2, "3": 3}'
>>>
OrderedDict
内部维护了一个双向链表
,它会根据元素加入的顺序来排列键的位置。第一个新加入的元素被放置在链表的末尾
。接下来对已存在的键做重新赋值不会改变键的顺序
。
OrderedDict的大小是普通字典的2倍多
,这是由于它额外创建的链表所致
。因此,如果打算构建一个涉及大量OrderedDict实例的数据结构
(例如从CSV文件中读取100000行内容到OrderedDict列表中),那么需要认真对应用做需求分析,是否可以用内存换便利
与字典有关的计算问题
「我们想在字典上对数据执行各式各样的计算(比如求最小值、最大值、排序等)。」
通常会利用zip()
将字典的键和值反转
过来
>>> prices={
... 'ACME':45.23,
... 'AAPL':612.78,
... 'IBM1':205.55,
... 'HPQ':37.20,
... 'FB':10.75
... }
>>>
>>> min(zip(prices.values(),prices.keys()))
(10.75, 'FB')
>>> max(zip(prices.values(),prices.keys()))
(612.78, 'AAPL')
>>>
同样,要对数据排序只要使用zip()
再配合sorted()就可以了
>>> sorted(zip(prices.values(),prices.keys()))
[(10.75, 'FB'), (37.2, 'HPQ'), (45.23, 'ACME'), (205.55, 'IBM1'), (612.78, 'AAPL')]
>>>
zip()
创建了一个迭代器,它的内容只能被消费一次,尝试对字典的值做计算。可以利用字典的values()方法
来解决这个问题:但是对于K的获取并不方便。
在计算min()和max()
时,如果碰巧value
的值相同,则将返回拥有最小或最大key值
的那个条目。
在两个字典中寻找相同点(交集)
「有两个字典,我们想找出它们中间可能相同的地方(相同的键、相同的值等)。」
关于字典的键
有一个很少有人知道的特性,那就是它们也支持常见的集合操作
,比如求并集、交集和差集。
如果需要对字典的键做常见的集合操作,那么就能直接使用keys-view
对象而不必先将它们转化为集合。获取到迭代器,可以直接对字典进行运算交集差集
>>> a = {
... 'x':1,
... 'y':2,
... 'z':3
... }
>>> b = {
... 'w':10,
... 'x':11,
... 'y':2
... }
>>> a.keys() & b.keys()
{'y', 'x'}
>>> a.keys() - b.keys()
{'z'}
字典的items()
方法返回由(key,value)对组成的items-view
对象。这个对象支持类似的集合操作,可用来完成找出两个字典间有哪些键值对有相同之处的操作。
>>> a.items() & b.items()
{('y', 2)}
>>>
也可以对字典进行过滤。
代码语言:javascript复制>>> {key:a[key] for key in a.keys() - {'z','W'}}
{'y': 2, 'x': 1}
>>>
字典的values()方法并不支持集合操作
。部分原因是因为在字典中键和值是不同的,从值的角度来看并不能保证所有的值都是唯一的。
从序列中移除重复项且保持元素间顺序不变
「我们想去除序列中出现的重复元素,但仍然保持剩下的元素顺序不变。」
如果序列中的值是可哈希(hashable)
的,那么这个问题可以通过使用集合
和生成器
轻松解决。
def dedupe(items):
seen = set()
for item in items:
if item not in seen:
yield item #生成有序列表
seen.add(item)
a = [1, 5, 2, 1, 9, 1, 5, 10]
print(list(dedupe(a)))
==========
[1, 5, 2, 9, 10]
如果没办法生成哈希值,如果一个对象是可哈希的,那么在它的生存期内必须是不可变的,它需要有一个hash()方法。
整数、浮点数、字符串、元组都是不可变的。
代码语言:javascript复制
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':1,'y':3},{'x':1,'y':2},{'x':2,'y':4}]
#转化为元组
print(list(dedupe(a,key=lambda b:(b['x'],b['y']))))
这里参数key的作用是指定一个函数用来将序列中的元素转换为可哈希的类型,这么做的目的是为了检测重复项。即行为参数化,lambda表达式的使用。