该内容已被发布者删除 该内容被自由微信恢复
文章于 2017年4月2日 被检测为删除。
查看原文
被用户删除
其他

Python 的计数方式发展史

2015-12-30 伯乐在线/叶和苏 Python开发者

(点击上方公号,可快速关注)


Python 解决问题的方式经常会随着时间的推移而发生改变。随着Python的演变,Python列表的计数方式也得到了发展。


对列表中各项目进行计数有各种各样的方法,接下来我们将通过观察每一种方法的代码风格,来分析这些技术的不同之处。至于它们的性能,我们以后再考虑。


我们需要了解一些 Python 的历史来理解这些不同的方法。幸运的是,我们可以用 Python 的 __future__ 模块,坐上时光机。现在让我们坐上德罗瑞恩(注:即系列电影《回到未来》中的时间机器),驶向1997年。


if 语句


现在是 1997 年 1 月,我们使用的是 Python 1.4。有一个颜色列表,我们很想要知道每一种颜色分别出现了多少次。我们来使用字典看看。


colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]

color_counts = {}

for c in colors:

if color_counts.has_key(c):

color_counts[c] = color_counts[c] + 1

else:

color_counts[c] = 1


注:我们没有用+=,因为增量赋值语句在 Python2.0 版之后才被添加进去。我们也没有使用 c in color_counts 语句,因为这个语句到 Python2.2 版本才可以使用。


运行上述代码后,我们可以看到 color_counts 字典里包含了每一种颜色出现的次数。


>>> color_counts

{'brown': 3, 'yellow': 2, 'green': 1, 'black': 1, 'red': 1}


这段代码很简单。通过循环遍历每一种颜色,检查每种颜色是否已经出现在字典中,如果还没有,把它添加到字典中。如果已经出现,那么把它的数量增一。


我们也可以按照下面的方式来写:


colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]

color_counts = {}

for c in colors:

if not color_counts.has_key(c):

color_counts[c] = 0

color_counts[c] = color_counts[c] + 1


对于稀疏列表(没有很多重复颜色的列表),这种方式也许会有一点点慢,因为在 for 循环里需要执行两条语句。但是我们不担心它的性能,我们考虑的是它的代码风格。深思熟虑之后,我们决定使用新版本的代码。


试试块语句


现在是 1997 年 1 月 2 日,我们仍然在使用 Python 1.4。某天的早晨,我们醒来后突然意识到,我们本来应该按照“先斩后奏” 的方式去实践(这种方式更符合 Python 的思想),但实际上,我们是按照“三思而后行”的想法在编写代码。现在按照“先斩后奏”的方式,把我们的代码重写成一个 try-except 的块语句:


colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]

color_counts = {}

for c in colors:

try:

color_counts[c] = color_counts[c] + 1

except KeyError:

color_counts[c] = 1


这段代码试着为每一种颜色的数量增一,但是如果这种颜色还没有出现在字典中,就会产生 KeyError 错误,那么该种颜色的数量就被初始化为 1。


get 方法


现在是 1998 年 1 月 1 日,Python 已经升级到 1.5 版本。我们决定用字典中的 get 方法来重写代码。


colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]

color_counts = {}

for c in colors:

color_counts[c] = color_counts.get(c, 0) + 1


这段代码循环遍历每一种颜色,每次循环从字典中获得当前颜色的计数,默认计数为零,然后计数加一,最后把新值赋给这个字典的键,也就是颜色。


这段代码很棒的是 for 循环里只有一行语句,但是我们并不确定这是不是更加符合 Python 的风格。我们觉得也许这次的改进显得太智能了,所以我们决定将这次的改进还原。


setdefault 方法


现在是 2001 年 1 月,我们已经在使用 Python 2.0 版。在 Python 2.0 版中,字典里新添加了 setdefault 的方法。于是我们决定用 setdefault 的方法和 += 的增量运算符来重写代码:


colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]

color_counts = {}

for c in colors:

color_counts.setdefault(c, 0)

color_counts[c] += 1


在每次循环里,不管 setdefault 的方法是否会被用到都会被调用一次,但是这次的更改可读性更强,而且比之前的方法更符合Python的代码风格,因此我们决定保留这次更改。


fromkeys方法


现在是 2004 年 1 月 1 日,我们在使用 Python 2.3。听说字典里添加了一种新的 fromkeys 方法,这种方法可以从序列中获取键值来创建字典。我们用这个新方法来重写代码:


colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]

color_counts = dict.fromkeys(colors, 0)

for c in colors:

color_counts[c] += 1


这段代码用颜色作为键创建了一个新的字典,并且每种颜色的数量被初始化为0。 这种方式可以直接增加每一种颜色的数量,不需要担心这种颜色有没有被包含到字典中。这段代码没有检查和例外情况的处理,我们认为这是一次改进,因此我们保留这次更改。


集合


现在是 2005 年 1 月 1 日,我们在使用 Python2.4。我们意识到现在可以使用集合(在 Python 2.3 版时发布,内置在 Python 2.4 版)和链表(在 Python 2.0 版时发布)来解决计数问题。再思考之后,我们突然想起生成器表达式也只是在 Python 2.4 时才公布,因此我们决定使用生成器表达式中的一种来进行计数。


colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]

color_counts = dict((c, colors.count(c)) for c in set(colors))


注:我们没有使用字典解析,因为字典解析到 Python2.7 版才可以使用。


这个方法不错,只用了一行代码。但是这符合 Python 的风格吗?


我们记得在《Python 之禅》中,从 python 列表的邮件线程开始介绍,到 Python 发展到 2.2.1 版时结束。在我们的交互式解释器中输入 import this:


>>> import this

The Zen of Python, by Tim Peters

Beautiful is better than ugly.

Explicit is better than implicit.

Simple is better than complex.

Complex is better than complicated.

Flat is better than nested.

Sparse is better than dense.

Readability counts.

Special cases aren't special enough to break the rules.

Although practicality beats purity.

Errors should never pass silently.

Unless explicitly silenced.

In the face of ambiguity, refuse the temptation to guess.

There should be one-- and preferably only one --obvious way to do it.

Although that way may not be obvious at first unless you're Dutch.

Now is better than never.

Although never is often better than *right* now.

If the implementation is hard to explain, it's a bad idea.

If the implementation is easy to explain, it may be a good idea.

Namespaces are one honking great idea -- let's do more of those!


这段代码的复杂度是 O(n2),与复杂度为 O(n) 的代码相比,这段代码的美观度和可读性更差。这次的改变只是一次很有趣的实验,但是这种一行式的方法更不符合 Python 的风格。因此我们决定将这次改变恢复原状。


defaultdict 方法


现在是 2007 年 1 月 1 日,我们在使用 Python2.5。我们刚刚发现 defaultdict 已经出现在 Python 的标准库中了。我们可以使用 defaultdict 将字典中的值初始化为 0。我们用 defaultdict 来重写这段代码


from collections import defaultdict

colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]

color_counts = defaultdict(int)

for c in colors:

color_counts[c] += 1


for 循环现在相当简单了。现在几乎符合 Python 的代码风格了。


在这段代码中,我们发现变量 color_counts 虽然跟字典不太一样,但是它继承了字典所有的映射功能。


>>> color_counts

defaultdict(<type 'int'>, {'brown': 3, 'yellow': 2, 'green': 1, 'black': 1, 'red': 1})


我们没有把 color_counts 这个类似于字典的对象转换成字典的格式,因为我们觉得以后的代码会更加的动态。


计数


现在是2011年1月,我们在使用Python 2.7。 我们了解到 defaultdict已经不是最符合 python 风格的计数方式了。在 Python 2.7 版的标准库中出现了一个 Counter 类,它可以为我们做所有的工作。


from collections import Counter

colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]

color_counts = Counter(colors)


还能变得更简单吗?这应该是最符合 Python 风格的代码了。


与 defaultdict 的方法一样,上面的代码返回一个类似字典的对象(实际上是字典的子类),这与我们的目的正好不谋而合,我们就使用这个方法了。


>>> color_counts

Counter({'brown': 3, 'yellow': 2, 'green': 1, 'black': 1, 'red': 1})


深思之后:性能


注意一下,我们并没有关注这些计数方法的运行效率。这些方法中大部分复杂度为 O(n),但是由于 Python 的实现方式不同,运行时间也有所差异。


尽管性能不是我们关注的主要方面,我还是很关心这些方法在 CPython 3.5.0 版下的运行时间。根据颜色在列表中出现的频率去观察每一种方法的运行时间是一件很有趣的事情。


结论


根据《Python之禅》(《Zen of Python》)中的格言,“应该只有一种,并且最好是唯一的方法”。 这只是一种期望。实际上并不总是只有一种显著的方法。方法是可以根据时间,需求和专业水平而发生改变的。


“Pythonic” 是相对的


相关资源


  • import this and the Zen of Python: 从这篇文章借鉴了Python 之禅

  • Permission or Forgiveness:Alex Martelli 论述了 Grace Hopper’s EAFP

  • Python How To: Group and Count with Dictionaries: 写这篇文章的时候,我发现了这篇相关的文章




【今日微信公号推荐↓】

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存