有一位计算机科学家说过,程序等于算法加数据结构,笔者也深以为然。在这篇文章中,笔者将通过一个完整的例子,一步一步来演示如何使用正确的数据结构。并在文章最后,推荐读者了解和学习一些常见的数据结构,以便在工作中恰到好处的使用,减少重复的工作,提升代码质量和工作效率。
1 合理使用数据结构
在正式介绍今天的例子之前,我们来热个身,看两个小问题。读者可以通过这两个小问题,看看自己对常用的数据结构,是否了解得足够仔细。
1.1 字典的get可以传递默认值
在工作中,我们经常会遇到这样的需求。从字典中获取某个key的值。如果key不存在,则给它赋一个默认值。对于这个需求,很多人是这么给参数赋默认值的:
port = kwargs.get('port')
if port is None:
port = 3306
其实,我们完全不用这么麻烦,因为,字典的get方法支持提供默认参数,在字典没有值的情况下,将会返回用户提供的默认参数,所以,优美的代码应该是这样的:
port = kwargs.get('port', 3306)
1.2 从列表中删除值
我们再来看一个类似的例子,获取元素且删除:
L = [1, 2, 3, 4]
last = L[-1]
L.pop()
这里,我们又多此一举了,因为,在调用L.pop()函数的时候,本身就会返回给我们需要pop的数据。也就是说,我们可以这样:
last = L.pop()
这里的两个小例子非常简单,但是,很容易被工程师忽略。笔者希望通过这两个例子来告诉大家,正确的使用数据结构的重要性。接下来,我们要看一个更加完整、更加复杂的例子。
2 使用字典统计单词出现的次数
我们要使用三种不同的方法统计单词出现的次数。大家可以在脑海里面想象一下,如果让你来统计一个文件中,各个单词的出现次数,你会怎么做?
最容易想到的是,通过字典来保存单词的出现次数。字典的key是单词,字典的value是单词出现的次数。但是,这里需要考虑的是,如果单词不存在于字典中,则我们需要对它赋一个初始值。如下所示:
d = {}
with open('/etc/passwd') as f:
for line in f:
for word in line.strip().split(':'):
if word not in d:
d[word] = 1
else:
d[word] += 1
3 使用defaultdict统计单词出现的次数
在前面的实现方法中,我们通过字典d来保存各个单词的出现次数。唯一不优美的地方在于,我们需要处理“边界”情况。即单词之前不存在于字典中的情况。有没有办法可以简化这个处理逻辑呢?答案是肯定的。在Python的标准库中,存在一个名为defaultdict的数据结构。该数据结构的作用,正如它的命名所反映的,可以赋一个默认值。例如,我们有一个字典,字典的值是一个列表,使用defaultdict的时,不用去判断key是否存在,直接进行列表操作即可。如下所示:
d = defaultdict(list)
for key, value in pairs:
d[key].append(value)
如果使用defaultdict来解决统计单词出现次数的问题,则可以优美的处理“边界”情况。并且,代码能够减少3行,会被认为更加Pythonic。如下所示:
d = defaultdict(int)
with open('/etc/passwd') as f:
for line in f:
for word in line.strip().split(':'):
d[word] += 1
4 使用Counter统计单词出现的次数
可能使用defaultdict以后,你已经很满意了。但是,对于这个问题,其实还有一个更好的解决办法,那就是使用collections中的Counter。如下:
word_counts = Counter()
with open('/etc/passwd') as f:
for line in f:
word_counts.update(line.strip().split(':'))
可以看到,使用Counter以后,我们的代码更加短小了。我们先把代码重8行重构到5行,然后又重构到4行。记住我们在第一篇文章中给出的实践方法:要想把代码写得优美,在保证可读性的前提下,代码越短越好。对于统计单词出现次数的例子,使用Counter还有其他的一些理由,那就是其他相关的需求。比如,现在还有第二个需求,打印出现次数最多的三个单词。如果我们使用字典,那么,我们需要这样:
result = sorted(zip(d.values(), d.keys()), reverse=True)[:3]
for val, key in result:
print(key, ':', val)
使用Counter就简单了,因为Counter直接就为我们提供了相应的函数,如下所示:
for key, val in (word_counts.most_common(3)):
print(key, ':', val)
是不是代码更短,看起来更加清晰呢?而且,统计每个单词出现的次数和出现次数最多的单词,这两个需求相关性实在是太强了,几乎会同时出现。所以,我们使用了Counter模块和该模块的most_common方法。如果Counter没有提供这个方法,那才是要被吐槽的!
我们使用了三种不同的方法来统计单词的出现次数,这个例子就充分说明了,善用标准库的重要性。我们再来看一个标准库的例子。
5 使用nametuple来简化代码
假设你现在要写一个监控系统,该系统要监控主机的方方面面。当然,包括了磁盘相关的监控。关于磁盘的信息,可以从/proc/diskstats
中获取磁盘的详细信息。/proc/diskstats
的内容如下所示:
"""
https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats
What: /proc/diskstats
The /proc/diskstats file displays the I/O statistics
of block devices. Each line contains the following 14
fields:
1 - major number
2 - minor mumber
3 - device name
4 - reads completed successfully
5 - reads merged
6 - sectors read
7 - time spent reading (ms)
8 - writes completed
9 - writes merged
10 - sectors written
11 - time spent writing (ms)
12 - I/Os currently in progress
13 - time spent doing I/Os (ms)
14 - weighted time spent doing I/Os (ms)
"""
$ cat /proc/diskstats
254 0 vda 24471 251 818318 33200 37026 64382 2289256 394516 0 24868 427704
254 1 vda1 24143 229 815530 33156 36072 64382 2289256 394428 0 24748 427572
254 32 vdc 614 0 22180 408 51203 2822 1857922 1051716 0 40792 1052064
/proc/diskstats
文件有很多列,如果我们使用下标访问的话,肯定需要借助我们的手指头。并且,也不一定能数清楚。就算你算术特别厉害,能够轻易地数清楚,如果下一个人要来修改你写的代码,对他来说,也不是一件轻松的事情。
作为一个有追求的程序员,我们当然要寻找更好的办法。对于这个问题,我们可以使用Python中的命名元组,也就是collections中的namedtuple。定义命名元组如下:
DiskDevice = collections.namedtuple('DiskDevice', 'major_number minor_number device_name read_count read_merged_count'
' read_sections time_spent_reading write_count write_merged_count '
'write_sections time_spent_write io_requests time_spent_doing_io'
' weighted_time_spent_doing_io')
有了命名元组以后,如果要返回某个磁盘的请求数据,就返回一个命名元组。调用者通过该命名元组,就能够通过属性的方式,而不是下标的方式访问各个字段。获取磁盘监控的代码如下:
def get_disk_info(disk_name):
with open("/proc/diskstats") as f:
for line in f:
if line.split()[2] == disk_name:
#返回给调用者的是一个命名元祖(namedtuple)
return DiskDevice(*(line.split()))
6 需要了解的数据结构
下面几个数据结构也是比较常用的数据结构,读者可以在https://pymotw.com/2/contents.html找到它们的使用教程。
- collections.deque 双端队列;
- heapq 堆数据结构;
- Queue 线程安全的队列;
- bisect 插入元素保持集合有序;
- OrderDict 有序字典,由于Python 3中的字典默认已经有序,因此,已经没有必要再学习OrderDict了。
7 总结
在这篇文章中,我们通过几个例子演示了正确使用数据结构的重要性。程序就是算法加数据结构,因此,很多问题,选择了正确的数据结构,就解决了一半的问题。为了选择合适的数据结构,我们需要知道Python中存在哪些数据结构,这些数据结构的方法怎么使用。希望这篇文章能够让读者领会到数据结构的重要性!
作者介绍
赖明星,架构师、作家。现就职于腾讯,参与并主导下一代金融级数据库平台研发。有多年的 Python 开发经验和一线互联网实战经验,擅长 C、Python、Java、MySQL、Linux 等主流技术。国内知名的 Python 技术专家和 Python 技术的积极推广者,著有《Python Linux 系统管理与自动化运维》一书。