0%

python cookbook(01):数据结构和算法

这篇文章将学习 Python 中数据结构与算法的常见编码技巧。

将序列分解为单独的变量

问题

将一个包含 N 个元素的元祖或序列,将它里面的值进行解压后,同时赋值给 N 个变量。

解决方案

  • 任何的可迭代对象都可以通过一个简单的赋值操作来分解为单独的变量。要求是变量的总数和结构必须与序列吻合
  • 如果元素数量与变量数量不匹配,会得到错误提示
  • 可以使用任意变量名去占位,丢弃那些不想要的变量值,常用的占位变量名为 _

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 元组分解
>>> x, y = (4, 5)
>>> x
4
>>> y
5

# 嵌套序列分解
>>> data = [1, 2, 3, (4, 5, 6)]
>>> x1, x2, x3, (y1, y2, y3) = data
>>> x1
1
>>> x2
2
>>> y1
4
>>> y3
6

# 字符串分解
>>> a, b = "hi"
>>> a
'h'
>>> b
'i'

# 生成器分解
>>> a, b = (i for i in (1, 2))
>>> a
1
>>> b
2

# 变量个数与序列元素个数不一致
>>> x, y = (1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)

# 使用占位符
>>> x, _, y, _ = [1, 2, 3, 4]
>>> x
1
>>> _
4
>>> y
3

解压可迭代对象赋值给多个变量

问题

对于一个可迭代对象,事先并不知道元素的数量,怎样才能从这个可迭代对象中解压中 N 个元素?

解决方案

Python 的星号表达式可以解决该问题。星号表达式可以让开发人员利用可迭代对象的元素构成规则,解压出想要的元素。

  • 需要注意,带星号的变量解压出来永远都是列表类型
  • 星号表达式可以用在任意位置
  • 如果想解压一些元素并直接丢弃它们,不能简单实用 *,而是需要使用 * + 占位变量,例如 *_

星号表达式在迭代可变长序列时特别有用,利用星号表达式在操作字符串的时候,也特别有用。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
>>> name, sex, *phone_numbers = ('liming', 'male', '10086', '10010')
>>> name
'liming'
>>> sex
'male'
>>> phone_numbers
['10086', '10010']

>>> name, sex, *phone_numbers = ('liming', 'male')
>>> phone_numbers
[]

>>> *trailing, current = range(10)
>>> trailing
[0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> current
9

>>> line = '_driverkit:*:270:270:DriverKit:/var/empty:/usr/bin/false'
>>> uname, *fields, homedir, sh = line.split(':')
>>> uname
'_driverkit'
>>> homedir
'/var/empty'
>>> sh
'/usr/bin/false'

>>> record = ('a', 50, 123.45, (12, 18, 2021))
>>> name, *_, (*_, year) = record
>>> name
'a'
>>> year
2021

>>> def sum(items):
... head, *tail = items
... return head + sum(tail) if tail else head
...
>>> sum(range(10))
45

保留最后 N 个元素

问题

在迭代操作或其他操作的时候,怎么只保留最后有限几个元素的历史记录?

解决方案

要想保留有限历史记录,可以使用 collections.deque。使用 deque(maxlen=N) 构造函数会创建一个固定大小的队列,当新的元素加入并且这个队列满的时候,最老的元素会自动被移除掉。虽然可以手动在一个列表上实现这一操作,但是使用队列方案更加优雅,并且运行更快。

  • 如果不设置队列大小,可以得到一个无限大小队列
  • 可以在队列两端执行添加、删除元素操作
  • 在队列两端插入或删除元素时间复杂度都是 O(1),而在列表开头插入或删除元素的时间复杂度为 O(N)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> q = deque(maxlen=3)
>>> q.append(1)
>>> q.append(2)
>>> q.append(3)
>>> q.append(4)
>>> q
deque([2, 3, 4], maxlen=3)
>>> q.append(5)
>>> q
deque([3, 4, 5], maxlen=3)

>>> q.pop()
5
>>> q
deque([3, 4], maxlen=3)
>>> q.popleft()
3
>>> q
deque([4], maxlen=3)

如下程序执行文本匹配,并返回匹配行的前 N 行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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__ == '__main__':
with open(r'test.txt') as f:
for line, previous_lines in search(f, 'python', 5):
for pline in previous_lines:
print(pline, end='')
print(line, end='')
print('-' * 20)

在这个例子中,函数 search 包含 yield 表达式,因此是一个生成器。这样编写代码可以让搜索过程代码和使用搜索结果的代码解耦。

查找最大或最小的 N 个元素

问题

从一个集合中获取最大或最小的 N 个元素。

解决方案

heapq 模块提供的 nlargest()nsmallest() 可以解决该问题。堆数据结构最重要的特征是 heap[0] 永远是最小的元素。

  • nlargest()nsmallest() 都可以接受一个关键字参数,用于操作复杂数据结构
  • heapify() 可以将一个列表转换为一个堆(就地转换)
  • heappop() 方法会将第一个元素弹出,然后用下一个最小的元素来取代被弹出的元素(时间复杂度 O(log N))

需要再正确的场景中使用 heapq 模块:

  • 当需要查找的元素个数较小时,函数 nlargest()nsmallest() 是合适的。
  • 如果想查唯一的最小或最大(N=1),使用 max()min() 更合适
  • 如果 N 大小和集合大小接近,则先对集合进行排序,再使用切片操作会更快一点

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> import heapq
>>> nums = [-1, 4, 5, -2, 3]
>>> print(heapq.nlargest(3, nums))
[5, 4, 3]
>>> print(heapq.nsmallest(3, nums))
[-2, -1, 3]

>>> students = [{'name': 'jack', 'grade': 100}, {'name': 'mark', 'grade': 90}, {'name': 'tim', 'grade': 95}]
>>> print(heapq.nlargest(2, students, key=lambda s: s['grade']))
[{'name': 'jack', 'grade': 100}, {'name': 'tim', 'grade': 95}]
>>> print(heapq.nsmallest(2, students, key=lambda s: s['grade']))
[{'name': 'mark', 'grade': 90}, {'name': 'tim', 'grade': 95}]

>>> nums = [-1, 4, 5, -2, 3]
>>> heapq.heapify(nums)
>>> nums
[-2, -1, 5, 4, 3]
>>> heapq.heappop(nums)
-2
>>> heapq.heappop(nums)
-1
>>> heapq.heappop(nums)
3
>>> nums
[4, 5]

实现一个优先级队列

问题

如何实现一个按优先级排序的队列呢?并且在该队列上每次 pop 操作总是返回优先级最高的那个元素。

解决方案

这里还是使用 heapq 模块:

  • heapq.heappush() 方法用于将一个元素插入到 heap 中,同时维持 heap 结构
  • heapq.heappop() 从 heap 中弹出最小的元素,同时维持 heap 结构

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 __str__(self):
return 'Item{!r}'.format(self._name)


if __name__ == '__main__':
q = PriorityQueue()
q.push(Item('foo'), 1)
q.push(Item('bar'), 5)
q.push(Item('sam'), 4)
q.push(Item('grok'), 1)

print(q.pop())
print(q.pop())
print(q.pop())
print(q.pop())

这里解释一下为什么插入队列的元组元素中包含 index,它主要有两个作用:

  • 当优先级相同时,可以继续基于 index 排序,而 index 总是递增的。因此当优先级相同时,总是按照元素插入顺序排序
  • 如果没有 index,当优先级相同时,会直接继续按照元素本身进行排序(这里就是 Item 实例),如果这些元素不支持排序,就会抛出异常

字典的中的键映射多个值

问题

怎么实现一个键能够对应多个值的字典(也叫 multidict)?

解决方法

一个字典就是一个键对应一个单值的映射。如果想要一个键映射多个值,需要将这些值放到另外的容器中:

  • 如果放到列表中,可以保持元素的插入顺序
  • 如果放到集合中,可以对重复元素去重

使用 collections 模块的 defaultdict,可以简化值的初始化。defaultdict 的一个特征是它会自动初始化每一个 key 刚开始对应的值。所以你只需要关注元素添加操作即可。

例如如果你使用普通的字典:

1
2
3
4
5
6
d = {}
for k, v in pairs:
# d.setdefault(k, []) 是一种等价写法
if k not in d:
d[k] = []
d[k].append(v)

而如果使用 defaultdict,则代码更加简单:

1
2
3
4
5
from collections import defaultdict

d = defaultdict(list)
for k, v in pairs:
d[k].append(v)

示例

1
2
3
4
5
6
7
8
9
10
>>> from collections import defaultdict

>>> d = defaultdict(list)
>>> d['a'].append(1)
>>> d['b'].append(1)
>>> d['a'].append(2)
>>> d['a']
[1, 2]
>>> d['b']
[1]

字典排序

问题

如果你想创建一个字典,同时希望在迭代或序列化该字典时能够控制元素的顺序。

解决方案

可以使用 collections 模块中的 OrderedDict 类,它会保持元素被插入时的顺序(如果用的条目覆盖现有条目,则原始插入位置不变)。OrderedDict 也提供 move_to_end 方法用于移动元素的顺序。另外,OrderedDict 之间的比较操作也是顺序敏感的,而 OrderedDict 与其他映射对象之间的相等性测试则与常规字典类似,对顺序不敏感。

OrderedDict 内部会维护着一个根据键插入顺序排序的双向链表。每次当一个新的元素插入进来时,它会被放到链表尾部。对于一个已经存在的键重复赋值不会改变键的顺序。也正是如此,OrderedDict 的大小是普通字典的两倍,因为它内部维持着一个链表。

当你想控制字典序列化(或编码成其他格式)后字段的顺序,OrderedDict 也是非常有用的。

示例

1
2
3
4
5
6
7
8
9
10
11
import json

d = OrderedDict()
d['foo'] = 1
d['bar'] = 2
d['spam'] = 3

for k in d:
print(k, d[k])

print(json.dumps(d))

字典的运算

问题

怎样在数据字典中执行一些计算操作,例如最小值、最大值、排序等等。

解决方案

当在字典上执行 min、max 等函数时,这些函数仅仅作用于键,而不是值:

1
2
3
4
5
>>> prices = {'A':1, 'B':3, 'C':2}
>>> min(prices)
'A'
>>> max(prices)
'C'

如果使用字典的 values() 方法,此时又无法获取到该 value 对应的 key:

1
2
3
4
>>> min(prices.values())
1
>>> max(prices.values())
3

当然,你可以通过如下复杂方式对字典元素的 value 进行比较,同时获取该元素的 key 和 value:

1
2
3
4
>>> k = min(prices, key=lambda k: prices[k])
>>> v = prices[k]
>>> k, v
('A', 1)

更简单的方式则是通过 zip() 函数将字典的 (key, vaule) 进行反转,之后再进行计算操作。这样就能实现 先基于 value 比较,然后再是 key

示例

1
2
3
4
5
6
>>> min(zip(prices.values(), prices.keys()))
(1, 'A')
>>> max(zip(prices.values(), prices.keys()))
(3, 'B')
>>> sorted(zip(prices.values(), prices.keys()))
[(1, 'A'), (2, 'C'), (3, 'B')]

当执行这些计算时,需要记住,zip() 创建的是只能访问一次的迭代器。所以每次需要重新使用 zip() 函数创建新的迭代器。

寻找两字典的相同点

问题

如何在两个字典中寻找相同的键,或者相同的元素。

解决方案

字典的 keys() 方法会返回一个展现键集合的键视图对象,键视图对象支持集合操作。所以可以直接对键视图对象执行集合操作,而不用先将其转化为 set。字典的 items() 方法返回一个包含 (key, value) 对的元素视图对象,其也支持集合操作。

但是字典的 values() 方法返回的对象不支持集合操作,其中一个原因是 值视图对象 不能保证所有的值互不相同,这样会导致集合操作出问题。如果的确想执行集合操作,可以将 值视图对象 转换成 set。

示例

1
2
3
4
5
6
7
8
9
10
11
>>> x = {'a':1, 'b':2, 'c':3}
>>> y = {'c':3, 'b':4, 'd':5}
>>> x.keys() & y.keys()
{'c', 'b'}
>>> x.keys() - y.keys()
{'a'}
>>> x.items() & y.items()
{('c', 3)}
>>> z = {key:y[key] for key in y.keys() - {'c'}}
>>> z
{'b': 4, 'd': 5}

删除序列相同元素并保持顺序

问题

如何在一个序列上保持元素顺序的同时消除重复的值。

解决方案

如果只是想消除重复元素,可以直接把序列构造成一个集合。但是这种方法不能维持元素的顺序。所以需要做些额外的工作:

1
2
3
4
5
6
def dedupe(items):
seen = set()
for item in items:
if item not in seen:
yield item
seen.add(item)

该方法只对元素中的序列为 hashable 的时候才管用。如果元素是不可哈希对象(例如 dict),可以提供一个 key 函数,将序列元素转换成 hashable 对象:

1
2
3
4
5
6
7
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)

示例

1
2
3
4
a = [1, 2, 3, 1, 2, 5]
print(list(dedupe(a)))
b = [{'x':1, 'y':2}, {'x':1, 'y':3}, {'x':1, 'y':2}]
print(list(dedupe(b, lambda d: (d['x'], d['y']))))
1
2
[1, 2, 3, 5]
[{'x': 1, 'y': 2}, {'x': 1, 'y': 3}]

命名切片

问题

硬编码的下标会使代码的可读性和可维护性下降,通过对硬编码的切片下标进行命名,可以提高程序可读性。

解决方案

内置的 slice() 函数可以创建一个切片对象,所有使用切片的地方均可以使用切片对象。

  • slice 对象提供三个属性:start、stop、step
  • 切片对象提供一个 indices(size) 方法,可以让切片对象根据指定的序列长度进行调整,并返回三个整数,分别表示调整后的 start、stop、len

示例

1
2
3
4
5
6
7
8
9
10
11
12
>>> items = [0, 1, 2, 3, 4, 4, 6]
>>> a = slice(2, 4)
>>> items[2:4]
[2, 3]
>>> items[a]
[2, 3]
>>> items[a] = [10, 11]
>>> items
[0, 1, 10, 11, 4, 4, 6]
>>> del items[a]
>>> items
[0, 1, 4, 4, 6]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> s = 'HelloWorld'
>>> a = slice(5, 100, 1)

>>> for i in range(a.start, a.stop, a.step):
... print(s[i])
...
W
o
r
l
d
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
IndexError: string index out of range

>>> for i in range(*a.indices(len(s))):
... print(s[i])
...
W
o
r
l
d

序列中次数最多的元素

问题

如何寻找一个序列中出现次数最多的元素。

解决方案

collections.Counter 类就是为了解决该类问题的。可以通过一个可迭代对象(元素必须是可哈希对象)或者映射对象来构造 Counter。一个 Counter 对象就是一个字典,元素就是字典的 key,元素出现的次数就是字典的 value。

  • 通过 most_common([n]) 方法可以直接获取出现次数排在前 n 的元素
  • Counter 对象还支持加法、减法、集合运算

示例

1
2
3
4
5
6
7
8
9
10
>>> 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']
>>> from collections import Counter
>>> word_counts = Counter(words)
>>> word_counts.most_common(3)
[('eyes', 8), ('the', 5), ('look', 4)]
>>> word_counts['around']
2
1
2
3
4
5
6
7
8
9
>>> morewords = ['why','are','you','not','looking','in','my','eyes']
>>> for w in morewords:
... word_counts[w] += 1
...
>>> word_counts['eyes']
9
>>> word_counts.update(morewords)
>>> word_counts['eyes']
10
1
2
3
4
5
6
7
8
9
10
11
12
>>> a = Counter(words)
>>> b = Counter(morewords)
>>> a
Counter({'eyes': 8, 'the': 5, 'look': 4, 'into': 3, 'my': 3, 'around': 2, 'not': 1, "don't": 1, "you're": 1, 'under': 1})
>>> b
Counter({'why': 1, 'are': 1, 'you': 1, 'not': 1, 'looking': 1, 'in': 1, 'my': 1, 'eyes': 1})
>>> a + b
Counter({'eyes': 9, 'the': 5, 'look': 4, 'my': 4, 'into': 3, 'not': 2, 'around': 2, "don't": 1, "you're": 1, 'under': 1, 'why': 1, 'are': 1, 'you': 1, 'looking': 1, 'in': 1})
>>> a - b
Counter({'eyes': 7, 'the': 5, 'look': 4, 'into': 3, 'my': 2, 'around': 2, "don't": 1, "you're": 1, 'under': 1})
>>> a & b
Counter({'my': 1, 'eyes': 1, 'not': 1})

通过某个关键字排序一个字典列表

问题

有一个字典列表,如何根据某个或某几个字典字段来排序这个列表。

解决方案

通过使用 operator 模块的 itemgetter 函数,可以非常容易实现按照某些字段对序列进行排序。sorted() 内置函数接受一个关键字参数,该参数是个可调用对象,该函数接受待排序序列中元素作为参数,返回用来排序的值。通过 itemgetter 可以返回的就一个可调用对象,通过该可调用对象,可以获得元素中的指定项。

  • itemgetter() 函数可以接受多个参数,从而返回一个元组
  • 通过 lambda 表达式也可以实现类似效果。但是 itemgetter() 会运行地更快一些
  • 该技术同样适用于 min()max() 等函数

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from operator import itemgetter

rows = 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'))
print(rows_by_fname)
print(rows_by_uid)

rows_by_lfname = sorted(rows, key=itemgetter('lname', 'fname'))
print(rows_by_lfname)

print(sorted(rows, key=lambda r: r['fname']))
print(sorted(rows, key=lambda r: (r['lname'], r['fname'])))

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

问题

对于某种类型,如果其原生不支持比较操作,如何对它们进行排序。

解决方案

内置的 sorted 函数支持一个关键字参数 key,它接受一个可调用对象,该可调用对象对每个传入的元素返回一个值,该值会被用来作为 sorted 函数的排序关键字。

  • 可以通过 lambda 函数实现 key
  • 也可以直接使用 operator.attrgetter() 来作为 key,其通常运行更快。
  • 该技术同样适用于 min()max() 函数

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from operator import attrgetter


class User:
def __init__(self, user_id):
self.user_id = user_id

def __repr__(self):
return 'User({})'.format(self.user_id)


def sort_notcompare():
users = [User(23), User(3), User(99)]
print(users)
print(sorted(users, key=lambda u: u.user_id))
print(sorted(users, key=attrgetter('user_id')))


sort_notcompare()
1
2
3
4
$ python3 sort_key.py
[User(23), User(3), User(99)]
[User(3), User(23), User(99)]
[User(3), User(23), User(99)]

通过某个字段将记录分组

问题

你如果有一个字典或者实例的序列,如何根据某个特定的字段来分组访问。

解决方案

itertools.groupby() 函数对于数据分组操作非常有用。groupby() 函数扫描整个序列并且查找连续相同值(或者根据 key 函数返回值相同)的元素序列。在每次迭代的时候,它会返回一个值和迭代器对象。通过该迭代器对象,可以对该分组里的元素进行迭代。因此使用 groupby() 的一个非常重要的准备步骤是根据指定的字段将数据排序

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from operator import itemgetter
from itertools import 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'},
{'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'},
]

rows.sort(key=itemgetter('date'))
for date, items in groupby(rows, key=itemgetter('date')):
print(date)
for i in items:
print(' ', i)
1
2
3
4
5
6
7
8
9
10
11
12
13
# python3 groupby.py
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'}

过滤序列元素

问题

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

解决方案

  • 最简单的过滤序列元素的方法就是列表推导。列表推导的缺点是:如果输入非常大的时候可能会产生很大的结果集,占用内存
  • 为了惰性生成序列元素,可以使用生成器表达式
  • 有时候过滤规则比较复杂,不能简单地在列表推导或者生成器表达式中表达出来。此时可以用内建的 filter() 函数。filter函数返回一个可迭代对象
  • 列表推导和生成器表达式还可以在过滤的时候对数据进行转换
  • 另外一个过滤工具是 itertools.compress(),它以一个 iterable 对象和一个对应的 Boolean 选择器序列作为输入参数。然后在 iterable 对象中输出对应选择器为 True 的元素。itertools.comporess() 的逻辑等效于:
1
2
3
def compress(data, selectors):
# compress('ABCDEF', [1,0,1,0,1,1]) --> A C E F
return (d for d, s in zip(data, selectors) if s)
  • 当需要用另一个相关联序列来过滤某个序列的时候,itertools.compress 还是非常有用的

示例

1
2
3
4
5
6
>>> n = [n for n in mylist if n > 0]
>>> n
[1, 4, 10, 2, 3]
>>> n = [n for n in mylist if n < 0]
>>> n
[-5, -7, -1]
1
2
3
4
5
>>> pos = (n for n in mylist if n > 0)
>>> pos
<generator object <genexpr> at 0x7fce353beff0>
>>> list(pos)
[1, 4, 10, 2, 3]
1
2
3
4
5
6
7
8
9
10
>>> def is_int(val):
... try:
... x = int(val)
... return True
... except ValueError:
... return False
...
>>> ivals = filter(is_int, values)
>>> print(list(ivals))
['1', '2', '-3', '4', '5']
1
2
3
>>> mylist = [1, 4, -5, 10, -7, 2, 3, -1]
>>> [n**2 for n in mylist]
[1, 16, 25, 100, 49, 4, 9, 1]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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]
>>> from itertools import compress
>>> list(compress(addresses, [n > 5 for n in counts]))
['5800 E 58TH', '1060 W ADDISON', '4801 N BROADWAY']

从字典中提取子集

问题

如果想构造一个字典,它是另一个字典的子集。

解决方案

解决该问题最简单的方案就是使用字典推导。除此之外,显式通过元组序列构建一个字典也可以实现该目的。但是字典推导意图更加清晰,而且运行也更快一些。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> prices = {
... 'ACME': 45.23,
... 'AAPL': 612.78,
... 'IBM': 205.55,
... 'HPQ': 37.20,
... 'FB': 10.75
... }
>>> p1 = {key:value for key,value in prices.items() if value > 200}
>>> p1
{'AAPL': 612.78, 'IBM': 205.55}
>>> tech_names = {'AAPL', 'IBM', 'HPQ', 'MSFT'}
>>> p2 = {key:value for key, value in prices.items() if key in tech_names}
>>> p2
{'AAPL': 612.78, 'IBM': 205.55, 'HPQ': 37.2}
1
2
3
4
5
6
>>> p3 = dict((key, value) for key, value in prices.items() if value > 200)
>>> p3
{'AAPL': 612.78, 'IBM': 205.55}
>>> p4 = dict((key, value) for key, value in prices.items() if key in tech_names)
>>> p4
{'AAPL': 612.78, 'IBM': 205.55, 'HPQ': 37.2}

映射名称到序列元素

问题

如果在代码中大量使用下标来访问列表或元组中的元素,会使得代码代码难以阅读。如果想通过名称来访问序列中的元素。

解决方案

collections.namedtuple() 函数是一个返回标准元组子类型的一个工厂方法。该函数接受一个类型名,以及该类型中的所有字段名,然后它返回一个类。通过使用返回类型,你就可以创建 命名元组 实例

  • 命名元组实例支持所有的普通元组操作,例如索引和解压
  • 命名元组的主要用于就是将你的代码从下标操作中解脱出来。下标操作会使代码表意不清晰,而且非常依赖记录的结构
  • 命名元组的另一个用途就是作为字典的替代,因为字典存储需要更多的内存空间。如果需要构建一个非常大的、包含字典的数据结构,那么使用命名元组会更加高效
  • 命名元组是不可更改的,如果真的需要改变属性的值,可以使用其 _replace() 方法,它会返回一个该命名元组的一个新的实例,在新的实例中使用新的属性值
  • 如果是需要一个包含许多实例属性的高效数据结构,命名元组并不是最佳选择,这个时候可以考虑使用一个包含 __slots__ 方法的类

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
>>> from collections import namedtuple
>>> Subscriber = namedtuple('Subscribe', ['addr', 'joined'])
>>> sub = Subscriber('test@test.com', '2022-12-03')
>>> sub
Subscribe(addr='test@test.com', joined='2022-12-03')
>>> sub.addr
'test@test.com'
>>> sub.joined
'2022-12-03'

>>> len(sub)
2
>>> a, b = sub
>>> a
'test@test.com'
>>> b
'2022-12-03'

>>> records = [('1@1.com', '2022-12-01'), ('2@2.com', '2022-12-02'), ('3@3.com', '2022-12-03')]
>>> for r in records:
... s = Subscriber(*r)
... print(s.addr, s.joined)
...
1@1.com 2022-12-01
2@2.com 2022-12-02
3@3.com 2022-12-03
1
2
3
4
5
6
7
8
>>> s1 = Subscriber('s1@s1.com', '2022-12-03')
>>> id(s1)
139804616657472
>>> s1 = s1._replace(addr='s1_new@s1.com')
>>> id(s1)
139804616657408
>>> s1
Subscribe(addr='s1_new@s1.com', joined='2022-12-03')

转换并同时计算数据

问题

你需要对转换后或过滤后的数据序列上执行聚集函数(例如 sum()min()max())。

解决方案

一种非常优雅的方式去结合数据转换和计算就是使用 生成器表达式

  • 当把生成器表达式作为一个单独的参数传递给函数时,并不需要多加一个括号
  • 使用生成器表达式作为参数会比先创建一个临时列表更加高效和优雅。生成器会以迭代的方式转换数据,更加节省内存

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
>>> nums = [1, 2, 3, 4, 5]
>>> s = sum([x * x for x in nums])
>>> s
55
>>> s = sum((x * x for x in nums))
>>> s
55
# 不需要额外的括号
>>> s = sum(x * x for x in nums)
>>> s
55

>>> import os
>>> files = os.listdir('.')
>>> if any(name.endswith('.py') for name in files):
... print('There be python!')
... else:
... print('No python!')
...
There be python!

>>> s = ('ACME', 50, 123.45)
>>> print(','.join(str(x) for x in s))
ACME,50,123.45

>>> min_shares = min(p['shares'] for p in portfolio)
>>> min_shares
20
>>> from operator import itemgetter
>>> min(portfolio, key=itemgetter('shares'))
{'name': 'AOL', 'shares': 20}

合并多个字典或映射

问题

有多个字典或者映射,想将它们从逻辑上合并为一个单一的映射后执行某些操作。

解决方案

collections 模块中的 ChainMap 类接受多个字典并将它们在逻辑上变为一个字典。这些字典并不是真的合并在一起了,ChainMap 类只是在内部创建了一个容纳这些字典的列表,并重新定义一些常见的字典操作来遍历这个列表:

  • 如果出现了重复键,那么第一次出现的映射值会返回
  • 对于字典的更新或删除操作,总是影响的是第一个列表的字典

ChainMap 对于编程语言中的作用范围变量(globals、locals)是非常有用的:

  • new_child() 方法会创建一个新的 ChainMap,其首先包含一个新的 map,然后再包含当前 ChainMap 实例中的剩余 map
  • parents 属性会创建一个新的 ChainMap,它会包含当前 ChainMap 实例中除第一个 map 外的所有 map

有的时候也会考虑使用 update() 方法将两个字典合并,但是需要注意:

  • 此时会创建一个新的字典
  • 如果原来字典做了更新,这种改变不会反应到新的合并字典中去。而 ChainMap 使用原来的字典,它自己不会创建新的字典

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
>>> a = {'x':1, 'z':3}
>>> b = {'y':2, 'z':4}
>>>
>>> from collections import ChainMap
>>> c = ChainMap(a, b)
>>> c['x']
1
>>> c['y']
2
>>> c['z']
3

>>> list(c.keys())
['y', 'z', 'x']
>>> list(c.values())
[2, 3, 1]

>>> c['z'] = 10
>>> c['w'] = 40
>>> del c['x']
>>> c.maps
[{'z': 10, 'w': 40}, {'y': 2, 'z': 4}]
>>> del c['y']
Traceback (most recent call last):
File "/usr/lib/python3.10/collections/__init__.py", line 1042, in __delitem__
del self.maps[0][key]
KeyError: 'y'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.10/collections/__init__.py", line 1044, in __delitem__
raise KeyError(f'Key not found in the first mapping: {key!r}')
KeyError: "Key not found in the first mapping: 'y'"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> values = ChainMap()
>>> values['x'] = 1
>>> values = values.new_child()
>>> values['x'] = 2
>>> values = values.new_child()
>>> values['x'] = 3
>>> values
ChainMap({'x': 3}, {'x': 2}, {'x': 1})
>>> values.maps
[{'x': 3}, {'x': 2}, {'x': 1}]

>>> values = values.parents
>>> values['x']
2
>>> values = values.parents
>>> values['x']
1
>>> values.maps
[{'x': 1}]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> a
{'z': 10, 'w': 40}
>>> b
{'y': 2, 'z': 4}
>>> merged = dict(b)
>>> merged.update(a)
>>> merged
{'y': 2, 'z': 10, 'w': 40}
>>> a['z'] = 20
>>> a
{'z': 20, 'w': 40}
>>> merged
{'y': 2, 'z': 10, 'w': 40}

>>> a
{'z': 20, 'w': 40}
>>> b
{'y': 2, 'z': 4}
>>> merged = ChainMap(a, b)
>>> a['w'] = 50
>>> merged['w']
50