文章目录
- Chap 1 数据结构与算法
- 1.1 解压序列赋值给多个变量
- 1.2 解压可迭代对象赋值给多个变量
- 1.3 保留最后 N 个元素 --- collections.deque
- 1.4 查找最大或最小的 N 个元素 --- heapq.nlargest/nsmallest
- 1.5 实现一个优先级队列 --- heapq.heappush/heappop
- 1.6 字典中的键映射多个值 --- collections.defaultdict
- 1.7 字典排序 --- collections.OrderedDict
- 1.8 对字典求最值和排序 --- zip()
- 1.9 查找两字典的相同点 --- key() & items() & Set Operation
- 1.10 删除序列相同元素并保持顺序 --- set() & yield
- 1.11 命名切片 --- slice()
- 1.12 序列中出现次数最多的元素 --- collections.Counter
- 1.13 通过关键字排序字典列表 --- operator.itemgetter for sorted/max/min
- 1.14 排序不支持原生比较的对象 --- sorted(a, key=) & operator.attrgetter()
- 1.15 通过某个字段将记录分组 --- itertools.groupby()
- 1.16 过滤序列元素 --- 列表解析, genExpr, filter(), itertools.compress()
- 1.17 从字典中提取子集 --- dict() & 字典解析
- 1.18 通过名称访问序列元素 --- collections.namedtuple()
- 1.19 转换并同时计算数据 --- genExpr as parameters
- 1.20 合并多个字典 or 映射 --- collections.ChainMap
Chap 1 数据结构与算法
1.1 解压序列赋值给多个变量
对任何序列 or 可迭代对象,将其 解压并赋值 给多个变量(要求:变量数 = 序列元素数)
p = (3, 6, 9)
x, y, z = p
data = ['sheldon', 180, (2020, 5, 1)]
name, height, date = data # 这里 date 将是一个 tuple
name, height, (year, month, date) = data
data = ['sheldon', 180, (2020, 5, 1)]
name, _, (year, _, _) = data # 只想获取 name 和 year 时,用任意变量名占位即可
1.2 解压可迭代对象赋值给多个变量
使用 星号表达式 实现 不确定个数 or 任意个 元素的解压 & 赋值
scores = [75, 80, 95, 67, 98, 92, 55, 73, 84]
first, *middle, last = socres # 此处 middle 是除去首尾两个分数的分数列表
print(avg(middle))
record = ('Einstein', 'Princeton', 233, 17, 273, 6)
name, university, *lucky_numbers = record # 此处变量 lucky_numbers 必为列表类型,不论幸运数字是多少个
scores = [95, 94, 93, 99, 60, 2] # 语,数,外,物理,化学,政治成绩
*other_scores, politics = scores # 用在列表开头的【星号表达式】
print(avg(other_scores)) # 政治除外的平均成绩
line = 'nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false'
user_name, *fields, homedir, sh = line.split(':') #
data = ['sheldon', 180, 'Cal Tech', (2020, 5, 1)]
name, *_, (year, *_) = data # 星号表达式 + 占位符 丢弃若干元素
1.3 保留最后 N 个元素 — collections.deque
使用 deque(maxlen=N) 来实例化一个固定大小的 队列。当新元素加入且该队列已满时, 最老的元素会被移除。
from collections import deque
# 1. 创建一个长度为 3 的队列
q = deque(maxlen=3)
# 2. 往队列中依次添加元素 1, 2, 3
for i in range(1, 4):
q.append(i)
# 3. 此时队列为: deque([1, 2, 3], maxlen=3)
print(q)
# 4. 往已满的队列中继续添加新元素
q.append('New Element')
# 5. 此时队列为: deque([2, 3, 'New Element'], maxlen=3)
print(q)
虽然使用列表可以实现同样操作,但队列的方案更加优雅且高效。另外在不指定队列的 maxlen 时,将得到一个无限长度的队列;可以在队列两端执行 添加 & 弹出元素 的操作。
# 1. 创建一个无限长度的队列
q = deque()
# 2. 添加元素 0, 1, 2
for i in range(0, 3):
q.append(i)
# 3. 此时为: deque([0, 1, 2])
print(q)
# 4. 从末尾添加元素
q.append(3)
# 5. 从开头添加元素
q.appendleft(-1)
# 6. 此时为: deque([-1, 0, 1, 2, 3])
print(q)
# 7. 从末尾弹出元素
q.pop()
# 8. 从开头弹出元素
q.popleft()
# 9. 此时为: deque([0, 1, 2])
print(q)
Remark: 在队列两端插入 or 删除元素の时间复杂度都是 O(1),而在列表的开头插入 or 删除元素の时间复杂度为 O(N)。
开头含生成器的例子可在 4.3 节后再看。
1.4 查找最大或最小的 N 个元素 — heapq.nlargest/nsmallest
从一个集合中获得最大 or 最小的 N 个元素列表可以通过 heapq 模块中的 nlargest 和 nsmallest 函数解决。
import heapq
nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
# 1. 通过 nlargest 与 nsmallest 函数从 nums 列表中分别找出最大与最小的三个元素
print(heapq.nlargest(3, nums)) # [42, 37, 23]
print(heapq.nsmallest(3, nums)) # [-4, 1, 2]
portfolio = [
{'name': 'IBM', 'shares': 100, 'price': 91.1},
{'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}
]
# 2. 对于更复杂的数据结构, 通过添加关键字参数可按指定字段获取最大 or 最小的 N 个元素
cheap = heapq.nsmallest(3, portfolio, key=lambda s: s['price'])
expensive = heapq.nlargest(3, portfolio, key=lambda s: s['price'])
print(cheap, expensive, sep="\n")
堆数据结构最重要的特征是:heap[0] 永远是最小的元素;剩余元素可通过调用 heapq.heappop 方法得到,该方法会先将首个元素弹出,然后用下一个最小的元素来取代被弹出元素,而且这种操作的时间复杂度仅是 O(log N),其中 N 是堆大小 。
nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
heap = list(nums)
heapq.heapify(heap) # 这里将 list 原地转化为堆
print(heap) # 此时 heap[0] 成为最小元素
# 不断调用 heappop 方法可逐个弹出当前最小元素
print(heapq.heappop(heap))
print(heapq.heappop(heap))
print(heapq.heappop(heap))
需要在正确场合使用函数 nlargest 和 nsmallest 才能发挥它们的优势:
情况 | 选择 |
---|---|
当要查找的元素个数相对较小时 | 函数 nlargest 和 nsmallest 性能更好 |
仅想查找最小 or 最大元素时 | min 和 max 函数会更快 |
当 N 的大小与集合大小接近时 | 先排序此集合,再使用切片操作会更快 |
1.5 实现一个优先级队列 — heapq.heappush/heappop
下面的类利用 heapq 模块实现了一个简单的优先级队列:
import heapq
class PriorityQueue:
def __init__(self):
self._queue = []
self._index = 0
def push(self, item, priority):
# heappush 函数将三元组 (-priority, self._index, item) 推入 self._queue 中
# 注意 heap[0](即这里的self._queue) 总是最小元素
heapq.heappush(self._queue, (-priority, self._index, item))
self._index += 1
def pop(self):
# heappop 函数从 heap 中弹出首个元素 heap[0]
return heapq.heappop(self._queue)[-1]
接下来展示它的使用:
class Item:
def __init__(self, name):
self.name = name
def __repr__(self):
""" 重写 __repr__(self) 方法可指定 print 实例时返回的内容。 """
return 'Item({!r})'.format(self.name)
# 1. 实例化一个 PriorityQueue 对象, 并向其中添加元素(指定优先级)
q = PriorityQueue()
q.push(Item('Leo'), 1)
q.push(Item('Raph'), 5)
q.push(Item('Mikey'), 4)
q.push(Item('Dony'), 1)
# 2. pop() 将按照优先级高低依次返回元素(当优先级相同时, 先返回优先插入的元素)
print(q.pop()) # Item('Raph')
print(q.pop()) # Item('Mikey')
print(q.pop()) # Item('Leo')
print(q.pop()) # Item('Dony')
1.6 字典中的键映射多个值 — collections.defaultdict
使用 collections 模块中的 defaultdict 来构造多值字典。
from collections import defaultdict
# 1. 需要保持顺序就选用 list
d = defaultdict(list)
d['a'].append(1)
d['a'].append(2)
d['b'].append(6)
d['b'].append(6)
print(d)
print(d['c'])
# 2. 需要去重就选用 set (不关心顺序的情况下)
b = defaultdict(set)
b['a'].add(1)
b['a'].add(2)
b['b'].add(6)
b['b'].add(6)
print(b)
print(b['c'])
从上面可见,defaultdict 会自动为将要访问的键(就算目前字典中并不存在这样的键)创建映射实体。
1.7 字典排序 — collections.OrderedDict
使用 collections 模块中的 OrderedDict 类可在迭代操作时保持元素被插入时的顺序。
from collections import OrderedDict
# 1. 实例化一个有序字典对象, 并插入一系列键值对
d = OrderedDict()
d['Leo'] = 1
d['Raph'] = 2
d['Mikey'] = 3
d['Dony'] = 4
d['Splinter'] = -1
# 2. 从打印结果可看出字典键值对的插入顺序在遍历时被保持
for key, value in d.items():
print(f"{key} --- {value}")
OrderedDict 内部维护着一个根据键插入顺序排序的双向链表。每当一个新元素插入进来的时候,它会被放到链表的尾部。对于一个已经存在的键的重复赋值不会改变键的顺序。
Remark:一个 OrderedDict 的大小是一个普通字典的两倍,因为它内部维护着另外一个链表,所以当构建一个需要大量 OrderedDict 实例的数据结构的时候,应该仔细权衡一下是否使用 OrderedDict 带来的好处要大过额外内存消耗的影响。
1.8 对字典求最值和排序 — zip()
按字典的 value 作为比较依据,求字典的最值:
prices = {'ACME': 45.23,
'AAPL': 612.78,
'IBM': 205.55,
'HPQ': 37.20,
'FB': 10.75,}
# 1. 使用 zip() 函数将字典 “反转” 为 (值, 键) 元组序列, 从而比较字典中元素的大小, 进而可以求其最值
min_price = min(zip(prices.values(), prices.keys()))
max_price = max(zip(prices.values(), prices.keys()))
print(max_price, min_price, sep="\n")
# 2. 类似地, 可以使用 zip() 和 sorted() 函数对字典数据排序
prices_sorted = sorted(zip(prices.values(), prices.keys()), reverse=True)
print(prices_sorted)
Remark:需要注意的是,zip() 函数创建的是一个只能访问一次的迭代器,也就是说 zip 一次就只能 print 一次, 再 print 之前必须重新 zip。
另外,上述方法当字典中的 value 有相同的值时,key 将决定返回结果:
ATK = {'Leonardo': 86,
'Raphael': 86}
# 攻击力相同, 但求最大/最小攻击力时结果却不同~
print(min(zip(ATK.values(), ATK.keys())))
print(max(zip(ATK.values(), ATK.keys())))
若尝试在字典上直接求最值,会发现结果只是按照 key 比较得到的最值;而使用了字典的 values() 方法,却又无法得到对应的 key:
print(min(prices)) # AAPL
print(max(prices)) # IBM
print(min(prices.values())) # 10.75
print(max(prices.values())) # 612.78
可以在 min() 和 max() 函数中提供关键字参数来获取最小值或最大值对应的键的信息如下:
key_for_min_price = min(prices, key=lambda s: prices[s])
key_for_max_price = max(prices, key=lambda s: prices[s])
print(key_for_min_price) # FB
print(key_for_max_price) # AAPL
# Remark: 这里的原理可能是, max(prices) 实际上是作用于 prices 的键值上, 关键字参数 key 通过匿名函数的方式构建了一个映射, 该映射将 max 的作用对象(也即 prices 的键值) 映射至 prices[键值] (这也就是字典中的值), 也即是说关键字参数 key 相当于指定 max() 函数按照 prices 的值做比较来求出最大值。
但是上面的方法仍然无法得到相应的最值,只能通过将上面的结果嵌套于 prices 中来获取相应的最值了:
print(prices[key_for_min_price]) # 10.75
print(prices[key_for_max_price]) # 612.78
1.9 查找两字典的相同点 — key() & items() & Set Operation
为寻找两字典的相同点,可直接在字典的 keys() or items() 方法の返回结果上【执行集合操作】。
a = {'x': 1, 'y': 2, 'z': 3}
b = {'x': 7, 'y': 2, 'w': 10}
# 1. 获取相同的键, 键的差集以及相同的键值对
print(f"Keys in common: {a.keys() & b.keys()}")
print(f"Keys in a and not in b: {a.keys() - b.keys()}")
print(f"(key, value) pairs in common: {a.items() & b.items()}")
# 2. 这些操作也可用于修改 or 过滤字典元素 (通过现有字典 a 构造新字典, 但排除键 z 和 w)
c = {key: a[key] for key in a.keys() - {'z', 'w'}} # 字典推导の写法
print(c)
Remark: 一个字典就是一个键集合与值集合的映射关系。
字典的 keys() & items() 的返回结果都支持集合运算,但 values() 的返回结果并不支持集合运算。
1.10 删除序列相同元素并保持顺序 — set() & yield
要在一个序列上保持元素顺序的同时消除重复值,可利用集合 or 生成器来实现,前提是:序列中的元素必须是 hashable 类型的。
def de_duplicate(items):
seen = set()
for item in items:
if item not in seen:
yield item
seen.add(item) # 这里确保只有不在 seen 集合中的元素才被 yield
a = [1, 5, 2, 1, 9, 1, 5, 10]
print(list(de_duplicate(a)))
当元素不可哈希时(如字典类型),要实现去重,可参考下面的做法:
def de_duplicate_2(items, key=None):
seen = set()
for item in items:
val = item if key is None else key(item) # key 参数指定一个函数将序列元素转换成 hashable 类型
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}]
# 这里的匿名函数将字典 d 映射到一个 tuple which is hashable
print(list(de_duplicate_2(a, key=lambda d: (d['x'], d['y']))))
# 这里的匿名函数将字典 d 映射到其值 value 上 which is hashable
print(list(de_duplicate_2(a, key=lambda d: d['x'])))
当只想单纯地去重而不在乎顺序的话,使用集合实现即可:
a = [1, 5, 2, 1, 9, 1, 5, 10]
print(set(a))
1.11 命名切片 — slice()
普通的切片可读性差 & 可维护性低,通过命名切片可使代码更加清晰易读:
Info = "Name:Sheldon; College:CalTech; Salary:$8,000 per month"
# 1. 内置的 slice() 函数创建了一个切片对象, 可用于任何允许使用切片的地方
name = slice(5, 12)
college = slice(22, 29)
print(f"name = {Info[name]}")
print(f"college = {Info[college]}")
下面是更多命名切片的例子:
items = [0, 1, 2, 3, 4, 5, 6]
a = slice(2, 4)
print(items[a]) # [2, 3]
items[a] = [18, 19]
print(items) # [0, 1, 18, 19, 4, 5, 6]
del items[a]
print(items) # [0, 1, 4, 5, 6]
# 2. 对切片对象调用其 start, stop, step 属性以获取更多信息
new = slice(5, 50, 4)
print(f"起点 = {new.start}")
print(f"终点 = {new.stop}")
print(f"步长 = {new.step}")
# 3. 调用切片的 indices(size) 方法, 可使切片对象自动适应一个序列, 以避免出现 IndexError 异常
s = "Hello World! I Love Python!"
# 将 new 应用于字符串 s 上时, 返回合适的 (start, stop, step)
print(new.indices(len(s)))
for i in range(*new.indices(len(s))):
print(s[i])
1.12 序列中出现次数最多的元素 — collections.Counter
需要找出一个序列中出现次数最多的元素时,Counter 类是一个极佳的选择:
from collections import Counter
words = ['look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes', 'the', 'eyes', 'the', 'eyes',
'the', 'eyes', 'not', 'around', 'the', 'eyes', "don't", 'look', 'around', 'the', 'eyes',
'look', 'into', 'my', 'eyes', "you're", 'under']
# 1. 使用列表实例化一个 Counter 类
word_counters = Counter(words)
# 2. 调用实例的 most_common(N) 方法获得频数
print(word_counters.most_common(3)) # [('eyes', 8), ('the', 5), ('look', 4)] --- (单词,频数)
Remark:作为输入,Counter 对象可接受任意序列对象,只要其元素是 hashable 的。
在底层实现上,一个 Counter 对象就是一个字典,它将元素映射到其频数上。
# 【增加计数】有两种方法:
print(word_counters['eyes'])
morewords = ['eyes', 'eyes', "bull's eye"]
# 1. 手动增加计数
for word in morewords:
word_counters[word] += 1
print(word_counters['eyes']) # 10
# 2. 通过 update() 方法增加计数
word_counters.update(morewords)
print(word_counters['eyes']) # 12
Counter 实例还有一个【鲜为人知の特性】,它们支持简单的数学运算:
a = Counter(words)
b = Counter(morewords)
print(a, b, sep="\n")
# 1. Combine counts (先对 key 做并集, 再对相应的 value 做加法)
c = a + b
print(f"c = {c}")
# 2. Subtract counts (先对 key 做交集, 再对相应的 value 做减法)
# (因此只在 b 中的键值对经过减法后也不会出现负的计数)
d = a - b
print(f"d = {d}")
1.13 通过关键字排序字典列表 — operator.itemgetter for sorted/max/min
通过使用 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}
]
# 1. 根据单个字段排序
rows_by_uid = sorted(rows, key=itemgetter('uid'))
rows_by_fname = sorted(rows, key=itemgetter('fname'))
print(rows_by_uid)
print(rows_by_fname)
# 2. 根据多个字段排序 (itemgetter 中位置越靠前的字段, 排序的优先级越高)
rows_by_lfname = sorted(rows, key=itemgetter('lname', 'fname'))
print(rows_by_lfname)
# 3. itemgetter() 有时候也可用 lambda 表达式代替:
rows_by_fname2 = sorted(rows, key=lambda r: r['fname']) # 匿名函数将字典映射到 fname 对应的 value
rows_by_lfname2 = sorted(rows, key=lambda r: (r['lname'], r['fname'])) # 匿名函数将字典映射到元组
print(rows_by_fname2)
print(rows_by_lfname2)
""" Remark: 虽然这种方法的效果和 itemgetter() 方法一致, 但性能上还是 itemgetter() 更优。"""
# 4. itemgetter() 不光适用于在 sorted() 函数中的 key 关键字, 在 max(), min() 函数中也能使用:
min_by_uid = min(rows, key=itemgetter('uid'))
max_by_lfname = max(rows, key=itemgetter('lname', 'fname'))
print(min_by_uid)
print(max_by_lfname)
1.14 排序不支持原生比较的对象 — sorted(a, key=) & operator.attrgetter()
想排序类型相同的对象,但它们不支持原生的比较操作时:内置的 sorted 函数有一个关键字参数 key ,可传入一个 callable 对象,该 callable 对象对每个传入的对象返回一个值,这个值会被 sorted 函数用来排序这些对象。
from operator import attrgetter
class User:
def __init__(self, user_id):
self.user_id = user_id
def __repr__(self):
return f"User({self.user_id})"
def sort_users():
users = [User(192), User(770), User(631), User(233)]
print(users)
print(sorted(users, key=lambda u: u.user_id))
# 此处的【lambda 表达式】作为 callable 对象, 对每个 User 实例返回该实例的 user_id,
# 于是 sorted() 函数按照这个 user_id 对 User 实例进行排序。
def sort_users_2():
users = [User(192), User(770), User(631), User(233)]
print(users)
print(sorted(users, key=attrgetter('user_id'))) # 通过 operator.attrgetter() 实现
sort_users()
sort_users_2()
使用【lambda 表达式】与 attrgetter 函数效果一致,但 attrgetter 性能更优,同时比较多个字段也很方便,这与 operator.itemgetter 函数作用于字典类型很类似(见1.13)另外,attrgetter 函数同样适用于 max,min 函数中。
1.15 通过某个字段将记录分组 — itertools.groupby()
根据某个特定字段 or 属性来 分组迭代访问 一个字典 or 实例序列:
from itertools import groupby
from operator import itemgetter
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'},
]
# >>>>>> 1. 想要在按 date 字段分组后的数据块上进行迭代:
# 1.1 首先需要按指定字段(此处为 date )排序
rows_sorted = sorted(rows, key=itemgetter('date'))
# 1.2 然后调用 groupby() 函数, 这里位置参数指定分组迭代对象, key 参数通过 itemgetter() 指定要分组的字段
for date, items in groupby(rows_sorted, key=itemgetter('date')):
print(date) # 分组后的日期
for item in items: # 此处 items 是个迭代器对象
print(f"\t{item}") # 每个日期组下的字典元素
# >>>>>> 2. 若只想根据 date 字段将数据分组到一个更大的数据结构中去, 并且允许随机访问:
from collections import defaultdict
# 2.1 实例化一个 value 为 list 类型的 defaultdict
rows_by_date = defaultdict(list)
for row in rows:
# 将 rows 中特定 date 的字典统统映射到其自身的 date
rows_by_date[row['date']].append(row)
# 2.2 defaultdict 中指定特定 date 后即可遍历得到所有该 date 下的字典
for each in rows_by_date['07/01/2012']:
print(each)
"""
第二个例子中, 没有必要先将记录排序。因此, 若对内存占用不很关心, 这种方式会比先排序后再通过 groupby() 函数迭代的方式运行得快一些。
"""
1.16 过滤序列元素 — 列表解析, genExpr, filter(), itertools.compress()
对一个序列,想利用一些规则从中提取出需要的值 or 缩短序列:
# 1. 最简单的过滤序列元素的方法就是使用列表解析
my_list = [1, 4, -5, 10, -7, 2, 3, -1]
your_list = [x for x in my_list if x > 0]
print(your_list)
his_list = [y for y in my_list if y < 0]
print(his_list)
使用列表解析的一个潜在缺陷就是当输入数据非常大的时候会产生一个非常大的结果集,占用大量内存。若对内存比较敏感,那么可以使用【生成器表达式】迭代产生过滤的元素。
# 2. 使用【生成器表达式】
positive = (x for x in my_list if x > 0) # This is a genExpr
print(positive) # It's a generator
for x in positive:
print(f"x = {x}")
# 3. 当过滤规则较复杂时, 可将过滤代码放到函数中, 然后使用内建的 filter() 函数进行过滤:
values = ['1', '2', '-3', '-', '4', 'N/A', '5']
def is_int(val):
try:
int(val) # 尝试将 val 转化为整数
return True
except ValueError:
return False
# filter(过滤规则, 待过滤对象) 函数创建了一个迭代器
int_vals = list(filter(is_int, values))
print(int_vals)
# 4. 列表解析 & 生成器表达式通常情况下是过滤数据最简单的方式, 其实它们还能在过滤的同时转换数据:
import math
nums = [x for x in range(1, 10)]
for num in (math.sqrt(y) for y in nums): # 在 genExpr 中对数字开方
print(num)
# 5. 过滤操作的一个变种就是将不符合条件的值用新值代替(也是一种转换数据), 而不是丢弃它们:
ints = [x for x in range(-5, 4)]
clip_pos = [x if x < 0 else 0 for x in ints]
clip_neg = [x if x > 0 else 0 for x in ints]
print(clip_pos, clip_neg, sep="\n")
另一个值得关注的过滤工具就是 itertools.compress 函数,它以一个 iterable 对象和一个相对应的 Boolean 选择器序列作为输入参数。然后输出 iterable 对象中对应选择器为 True 的元素。当需要用另外一个相关联的序列来过滤某个序列的时候,这个函数非常有用:
# 6. 要将对应 count 值大于 5 的地址全部输出:
from itertools import compress
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]
# 6.1 关键在于先创建一个 Boolean 序列, 指示哪些元素符合条件
greater_than_5 = [(x > 5) for x in counts] # 这里面都是 True & False
# 6.2 然后 compress(待过滤对象, Boolean 选择器) 根据该 Boolean 序列去选择输出对应位置为 True 的元素
chosen = list(compress(addresses, greater_than_5)) # filter() & compress() 都返回一个迭代器
print(chosen)
1.17 从字典中提取子集 — dict() & 字典解析
通过 字典解析 可以方便地从现有字典获取子字典:
prices = {'ACME': 45.23,
'AAPL': 612.78,
'IBM': 205.55,
'HPQ': 37.20,
'FB': 10.75, }
# 1. price > 200 的键值对所成的字典
p1 = {key: value for key, value in prices.items() if value > 200}
# 2. key 在某个集合中的键值对所成的字典(2 ways)
tech_names = {'AAPL', 'IBM', 'HPQ', 'MSFT'}
p2 = {key: value for key, value in prices.items() if key in tech_names}
p2_ = {key: prices[key] for key in prices.keys() & tech_names} # 这里用到了交集
print(p1, p2, p2_, sep="\n")
# 3. 通过创建一个元组序列然后将其传给 dict() 函数也能实现上述要求
p3_ = [(key, value) for key, value in prices.items() if value > 300]
p3 = dict(p3_)
print(p3)
相比最后一种方法,字典解析的方式更清晰易懂,并且实际上也会运行的更快些。
1.18 通过名称访问序列元素 — collections.namedtuple()
你有一段【通过下标访问列表 or 元组中元素】的代码,但这样有时会使你的代码难以阅读,于是你想通过名称来访问元素:
from collections import namedtuple
# >>>>>> 1. 通过名称来访问 list or tuple 的元素
# 1.1 传递 类型名, 字段列表 以创建命名元组类 Subscriber
Subscriber = namedtuple('Subscriber', ['addr', 'joined'])
# 1.2 传递字段值 以实例化命名元组类
sub = Subscriber('GS@example.com', '2020-08-18')
# 1.3 通过字段名访问元素
print(sub, sub.addr, sub.joined, sep="\n")
# 2. >>>>>> 命名元组与元组类型是【可交换的】, 它支持所有普通元组操作, 如索引和解压
# 2.1 命名元组の长度
print(f"命名元组 sub 的长度为: {len(sub)}")
# 2.2 解压一个命名元组
addr, joined = sub
print(f"addr = {addr}; joined = {joined}")
命名元组的一个主要用途是将代码从下标操作中解脱出来。 如果你从数据库调用中返回了一个很大的元组列表,通过下标去操作其中的元素,当你在表中添加了新的列时你的代码可能就会出错了。但如果你使用了命名元组,那么就不会有这样的顾虑:
# 1. 使用普通元组的代码:
def compute_cost(records):
""" 下标操作通常会让代码表意不清晰, 并且非常依赖记录的结构 """
total = 0.0
for record in records:
# 通过下标访问元素, 代码正确性依赖于 record 的结构
total += record[1] * record[2]
return total
# 创建命名元组类 Stock
Stock = namedtuple('Stock', ['name', 'shares', 'price'])
# 2. 使用命名元组的代码:
def compute_cost_2(records):
total = 0.0
for record in records:
# 将 record 中元素解压赋值给 Stock 的位置参数, 得到一个命名元组实例 s
s = Stock(*record)
# 通过字段名访问元素, 不依赖于 record 的结构
total += s.shares * s.price
return total
命名元组另一个用途就是作为字典的替代,因为字典存储需要更多的内存空间。如果要构建一个非常大的包含字典的数据结构,那么使用命名元组会更高效。但需注意的是,与字典不同,命名元组是不可更改的。
# 1. 创建命名元组类 Stock
Stock = namedtuple('Stock', ['name', 'shares', 'price'])
# 2. 实例化该命名元组类
s = Stock('ACME', 100, 123.45)
print(s)
# 3. 创建命名元组实例后, 像 s.shares = 75 这样尝试更改其字段值会报错!如果一定要改变属性的值, 那么可使用命名元组实例的 _replace() 方法, 它将【创建一个全新的命名元组】并【将对应的字段用新的值取代】。
s = s._replace(shares=75)
print(s)
_replace() 方法还有一个很有用的特性:当命名元组有可选 or 缺省字段时,它是一个非常方便的填充数据的方法。可以先创建一个包含缺省值的原型元组,然后使用 _replace() 方法创建新的【值被更新过的】实例。
# 1. 创建命名元组类
Stock = namedtuple('Stock', ['name', 'shares', 'price', 'date', 'time'])
# 2. 创建 prototype 实例
stock_prototype = Stock('', 0, 0.0, None, None)
# 3. 通过 _replace() 方法, 定义一个将字典转化为命名元组实例的函数
def dict_to_stock(s):
return stock_prototype._replace(**s) # 将字典作为关键字参数传递?
# 4. 将字典转化为命名元组实例, prototype 中的缺省值被新值取代了
a = {'name': 'ACME', 'shares': 100, 'price': 123.45}
b = {'name': 'ACME', 'shares': 100, 'price': 123.45, 'date': '12/17/2012'}
a_ = dict_to_stock(a)
b_ = dict_to_stock(b)
print(a_, b_, sep="\n")
最后要说的是,如果你的目标是定义一个需要更新很多实例属性的高效数据结构,那么命名元组并非最佳选择。这时你应该考虑定义一个包含 _ _ slots _ _ 方法的类(参考 8.4 小节)。
1.19 转换并同时计算数据 — genExpr as parameters
你需要在数据序列上执行聚集函数(如 sum,min,max 等),但首先你需要转换 or 过滤数据,此时使用【生成器表达式参数】将是一个非常优雅的【结合数据计算 & 转换】的方式,下面是一些例子:
import os
# >>>>>> 1. 求平方和
nums = [1, 2, 3, 4, 5]
# 1.1 将一个【生成器表达式】当作参数传递给 sum (这里 genExpr 的括号其实可以省略, 那样的代码更优雅)
s = sum((x**2 for x in nums))
print(s)
# >>>>>> 2. Determine if any .py files exist in a directory
# 2.1 os.listdir() 方法返回 “指定路径下” 【所有文件 & 文件夹 の 名字】所成的列表
files = os.listdir('dirname')
# 2.2 any() 中使用了 genExpr 参数
if any((name.endswith('.py') for name in files)):
print('There be python!')
else:
print('Sorry, no python.')
# >>>>>> 3. Output a tuple as CSV
s = ('ACME', 50, 123.45)
# 3.1 将 tuple 中每个元素转化为字符串, 然后以逗号分隔地拼接成一个字符串
print(','.join((str(x) for x in s)))
# >>>>>> 4. Data reduction across fields of a data structure
portfolio = [
{'name': 'GOOG', 'shares': 50},
{'name': 'YHOO', 'shares': 75},
{'name': 'AOL', 'shares': 20},
{'name': 'SCOX', 'shares': 65}
]
# 4.1 将 genExpr 作为参数传递给 min 函数
min_shares = min(s['shares'] for s in portfolio) # 这里舍弃了 genExpr 的括号
print(min_shares)
使用一个生成器表达式作为参数会比先创建一个临时列表更加高效和优雅。比如,如果不使用生成器表达式的话,你可能会考虑使用下面的实现方式求平方和:
nums = [1, 2, 3, 4, 5]
sum([x * x for x in nums]) # 通过列表解析先创建了一个列表, 再传递给 sum 函数
上述做法同样可以达到想要的效果,但它会多一个步骤 —— 先创建一个额外的列表。对于小型列表可能没什么关系,但如果元素数量非常大的时候,它会创建一个巨大的仅仅被使用一次就被丢弃的临时数据结构。而生成器方案会以迭代的方式转换数据,因此更省内存。
1.20 合并多个字典 or 映射 — collections.ChainMap
现在有多个字典 or 映射,想将它们在逻辑上合并为一个单一の映射后执行某些操作,如查找值 or 检查某些键是否存在。一个非常简单的解决方案就是使用 collections 模块中的 ChainMap 类:
# 1. 通过两字典 a, b 实例化一个 ChainMap 对象
c = ChainMap(a, b)
print(f"c = {c}")
# 2. 注意这里返回的 c['z'] 是 a 中的值 (因为查找会返回在 ChainMap 对象中优先出现的映射)
print(f"c['x'] = {c['x']}; c['y'] = {c['y']}; c['z'] = {c['z']}")
一个 ChainMap 接受多个字典并将它们在逻辑上变为一个字典。然而这些字典并非真的合并在一起了,ChainMap 类只是在内部创建了一个容纳这些字典的列表并重新定义了一些常见的字典操作来遍历这个列表。大部分字典操作都可以正常使用:
print(len(c)) # 长度
print(list(c.keys())) # keys
print(list(c.values())) # values
print(list(c.items())) # (key, value)s
c['z'] = 10 # 更新键值对
c['w'] = 40 # 新增键值对
del c['x'] # 删除键值对
# 3. 对字典の【更新 or 删除】操作总是影响第一个字典 (即实例化 ChainMap 时的第一个字典 a)
print(f"传递给 ChainMap 的首个字典 a 变成了: {a}")
# 4. del c['y'] 这条语句会报错, 因为原本 a 中没有 y 这个键
关于【ChainMap 对于编程语言中的作用范围变量(如 globals,locals 等)是非常有用的。】这部分我还没看懂,因此这里先跳过。—— Page 45
另外,作为 ChainMap 的替代,你可能会考虑使用 update() 方法将两个字典合并:
a = {'x': 1, 'z': 3}
b = {'y': 2, 'z': 4}
# 1. 创建一个新的字典对象
merged = dict(b)
print(f"update 前, merged = {merged}")
# 2. 在 merged 的基础上, 更新加入 a 中的键值对
merged.update(a)
print(f"update 后, merged = {merged}") # 注意这里 z 的值由 4 变成了 3
# 3. 同时, 若更新了原字典, 这种改变不会反应到通过 update() 合并的字典中去:
a['x'] = 233
print(merged['x']) # 结果是 1 而不是 233
但是通过 ChainMap 在逻辑上合并的字典,能够反映在原字典上做的更新:
a = {'x': 1, 'z': 3}
b = {'y': 2, 'z': 4}
merged = ChainMap(a, b)
print(merged['x']) # 1
# 这里更新原字典, 发现 ChainMap 中的键值对也相应更新了
a['x'] = 233
print(merged['x']) # 233