资讯专栏INFORMATION COLUMN

Python语言中计数方法的演变

sunsmell / 1952人阅读

摘要:译文链接编程派有时候,利用语言简洁优雅地解决问题的方法,会随着时间变化。随着不断进化,统计列表元素数量的方法也在改变。最后将字典中相应键的值设置为新的计数。我们发现这种方法比之前的代码更加简洁优雅,所以提交了此次修改。

文中如对专业术语的翻译有误,请Python高手指正(Pythonistas),谢谢!另外,原文中的Pythonic一词,大意指符合Python语言规范、特性和数据结构的编码方式,蕴涵较为丰富,为了行文更顺畅、容易理解,此文暂时用简洁、优雅代替。-- EarlGrey@编程派

原文链接:Trey Hunner,发布于11月9日。
译文链接:编程派

有时候,利用Python语言简洁、优雅地解决问题的方法,会随着时间变化。随着Python不断进化,统计列表元素数量的方法也在改变。

以计算元素在列表中出现的次数为例,我们可以编写出许多不同的实现方法。在分析这些方法时,我们先不关注性能,只考虑代码风格。

要理解这些不同的实现方式,我们得先知道一些历史背景。幸运的是,我们生活在"__future__"世界,拥有一台时间机器。接下来,我们一起坐上时光机,回到1997年吧。

if 语句

1997年1月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

注意:我们没有使用+=,因为增量赋值直到Python 2.0才出现;另外,我们也没有使用c in color_counts这个惯用法(idiom),因为这也是Python 2.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

如果列表稀疏度高(即列表中不重复的颜色数量很多),这段代码可能运行的会有点慢。因为我们现在要执行两个语句,而不是一个。但是我们不关心性能问题,我们只关注编码风格。经过思考,我们决定采用新版的代码。

try代码块(Code Block)

1997年1月2日,我们使用的还是Python 1.4。今早醒来的时候,我们突然意识到:我们的代码遵循的是“三思而后行”(Look Before You Leap,即事先检查每一种可能出现的情况)原则,但实际上我们应该按照“获得谅解比获得许可容易”(Easier to Ask Forgiveness, Than Permission,即不检查,出了问题由异常处理来处理)的原则进行编程,因为后者更加简洁、优雅。我们用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

现在,我们的代码会遍历每种颜色,从字典中获取该颜色的当前计数值。如果没有这个计数值,则该颜色的计数值默认为0,然后在数值的基础上加1。最后将字典中相应键的值设置为新的计数。

把主要代码都写在一行里,感觉很酷,但是我们不敢完全肯定这种做法更加简洁、优雅。我们觉得可能有点太聪敏了,所以还是撤销了这次的重构。

setdefault方法

2001年1月1日,我们现在使用的是Python 2.0。我们听说字典类型现在有一个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方法。但这样做,的确会让代码看上去可读性更高。我们发现这种方法比之前的代码更加简洁、优雅,所以提交了此次修改。

fromkeys方法

2004年1月1日,我们使用的是Python 2.3。我们听说字典新增了一个叫fromkeys的类方法(class method),可以利用列表中的元素作为键来构建字典。我们使用新方法重构了代码:

colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]
color_counts = dict.fromkeys(colors, 0)
for c in colors:
    color_counts[c] += 1

这段代码将不同的颜色作为键,创建了一个新的字典,每个键的值被默认设置为0。这样,我们增加每个键的值时,就不用担心是否已经进行了设置。我们也不需要在代码中进行检查或异常处理了,这看上去是个改进。我们决定就这样修改代码。

推导式(Comprehension)与集合

2005年1月1日,我们现在用的是Python 2.4。我们发现可以利用集合(Python 2.3中发布,2.4版成为内置类型)与列表推导式(Python 2.0中发布)来解决计数问题。进一步思考之后,我想起来Python 2.4中还发布了生成器表达式(generator expressions),我们最后决定不用列表推导式,而是采用生成器表达式。

colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]
color_counts = dict((c, colors.count(c)) for c in set(colors))

注意:我们这里使用的不是字典推导式,因为字典推导式直到Python 2.7才被发明。

运行成功了,而且只有一行代码。但是这种代码够简洁、优雅吗 ?

我们想起了Python之禅(Zen of Python),这个Python编程指导原则起源于一个Python邮件列表,并悄悄地收进了Python 2.2.1版本中。我们在REPL(read-eval-print loop,交互式解释器)界面中输入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! # 命名空间是一个响亮的出色想法–就让我们多用用它们

译者注:Python之禅的翻译版本很多,这里选用的译文出自啄木鸟社区。

我们的代码变得更复杂,时间复杂度从O(n)增加到了O(n2);还变的更丑,可读性更差了。我们那样改,是一次有趣的尝试,但是一行代码的实现形式,没有我们之前的方法简洁、优雅。我们最后还是决定撤销修改。

defaultdict方法

2007年1月1日,我们使用的是Python 2.5。我们刚发现,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循环现在变得真简单!这样肯定是更加简洁、优雅了。

我们发现,color_counts这个变量的行为现在有点不同,但是它的确继承了字典的特性,支持所有相同的映射功能。

color_counts
defaultdict(, {"brown": 3, "yellow": 2, "green": 1, "black": 1, "red": 1})

我们在这里没有把color_counts转换成字典,而是假设其他的代码也使用鸭子类型(duck typing, Python中动态类型的一种,这里的意思是:其他代码会将color_counts视作字典类型),不再改动这个类似字典的对象。

Counter

2011年1月1日,我们使用的是Python 2.7。别人告诉我们,之前使用defaultdict编写的代码,不再是统计颜色出现次数最简洁、优雅的方法了。Python 2.7中新引入了一个Counter类,可以完全解决我们的问题。

from collections import Counter
colors = ["brown", "red", "green", "yellow", "yellow", "brown", "brown", "black"]
color_counts = Counter(colors)

还有比这更简单的方法吗?这个一定是最简洁、优雅的实现了。

defaultdict一样,Counter类返回的也是一个类似字典的对象(实际是字典的一个子类)。这对满足我们的需求来说足够了,所以我们就这么干了。

color_counts
Counter({"brown": 3, "yellow": 2, "green": 1, "black": 1, "red": 1})
性能考虑

请注意,在编写这些实现方式时,我们都没有关注效率问题。大部分方法的时间复杂度相同(O(n)),但是不同的Python语言实现形式(如CPython, PyPy,或者Jython)下,运行时间会有差异。

尽管性能不是我们的主要关注点,我还是在CPython 3.5.0的实现下测试了运行时间。从中,你会发现一个有趣的现象:随着列表中颜色元素的密度(即相同元素的数量)变化,每一种实现方法的相对效率也会不同。

结语

根据Python之禅,“ 应该有一个——宁肯只有一个明显的实现方法”。这句话所说的状态值得追求,但事实是,并不总是只有一种明显的方法。这个“明显”的方法会随着时间、需求和专业水平,不断地变化。

“简洁、优雅”(即Pythonic)也是一个相对的概念。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/44193.html

相关文章

  • Python 学习资料整理

    摘要:整理可爱的简化元编程的是什么鬼,多线程性能究竟如何浅谈的语句基础深入理解中的赋值引用拷贝作用域命名空间和作用域窥探引用计数编写漂亮的,可读的代码的最佳时间语言中计数方法的演变描述符解密的内存管理机制深入的内存管理源码剖析 Python 整理 Python3 Official Documentation Python3 Document Coding Style PEP 8 Encodi...

    pf_miles 评论0 收藏0
  • 细数Python三十年技术演变史——萌发于小众,崛起于AI

    摘要:作者宋天龙来源科技大本营导语一切都始于年的那个圣诞节,的诞生并不算恰逢其时,它崛起充满了机遇巧合,也有其必然性。年的圣诞节,开始编写语言的编译器。年发布的标志着的框架基本确定。年月发布了系列的最后一个版本,主版本号为。 showImg(https://segmentfault.com/img/remote/1460000019862276); 作者 | 宋天龙来源 | AI科技大本营 ...

    tuniutech 评论0 收藏0
  • 能让你更早下班Python垃圾回收机制

    摘要:内部通过引用计数机制来统计一个对象被引用的次数。下一步,就该被我们的垃圾回收器给收走了。而我们垃圾回收机制只有当引用计数为的时候才会释放对象。以空间换时间的方法提高垃圾回收效率。 人生苦短,只谈风月,谈什么垃圾回收。据说上图是某语言的垃圾回收机制。。。我们写过C语言、C++的朋友都知道,我们的C语言是没有垃圾回...

    pumpkin9 评论0 收藏0
  • looking at python

    摘要:代码易于阅读,并且,易于维护这一条是看来的,这可能只是一种感觉问题。因此的垃圾回收机制由一个引用计数器和一个循环垃圾收集器组成。类的继承支持多重继承父类的构造函数必须在子类中亲自调用 Python 初探 Python是。。? python是一种解释型脚本语言。 python语言是一句一句执行的,可以再命令提示符中直接编写程序并随时获得结果 python支持面向对象 我已经会c了,除...

    Channe 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<