合理使用数据结构 #P002#

有一位计算机科学家说过,程序等于算法加数据结构,笔者也深以为然。在这篇文章中,笔者将通过一个完整的例子,一步一步来演示如何使用正确的数据结构。并在文章最后,推荐读者了解和学习一些常见的数据结构,以便在工作中恰到好处的使用,减少重复的工作,提升代码质量和工作效率。

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 系统管理与自动化运维》一书。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值