当前位置: 首页 > 科技观察

Python语言中计数方法的演进

时间:2023-03-18 21:00:24 科技观察

有时,使用Python语言简洁优雅地解决问题的方法会随着时间的推移而发生变化。随着Python的发展,计算列表元素的方法也在发展。以统计一个元素在列表中出现的次数为例。我们可以编写许多不同的实现。在分析这些方法时,我们首先不关注性能,只考虑代码风格。要理解这些不同的实现,我们需要一些历史背景。幸运的是,我们生活在“未来”世界,拥有时光机。接下来,让我们一起乘坐时光机回到1997年,if语句1997年1月1日,我们使用的是Python1.4。现在有一个不同颜色的列表,我们想知道每种颜色在列表中出现的次数。让我们用字典做数学吧!colors=[“brown”,“red”,“green”,“yellow”,“yellow”,“brown”,“brown”,“black”]color_counts={}forcincolors:ifcolor_counts.has_key(c):color_counts[c]=color_counts[c]+1else:color_counts[c]=1注意:我们没有使用+=,因为增量赋值直到Python2.0才出现;另外,我们没有使用cincolor_counts惯用方法(idiom),因为它是在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={}forcincolors:ifnotcolor_counts.has_key(c):color_counts[c]=0color_counts[c]=color_counts[c]+1如果列表是稀疏的(即列表中不重复的颜色数量多),这段代码可能运行得有点慢。因为我们现在执行的是两条语句而不是一条。但是我们不关心性能问题,我们只关心编码风格。思前想后,我们决定采用新版本的代码。尝试代码块(CodeBlock)1997年1月2日,我们仍在使用Python1.4。当我们今天早上醒来时,我们才恍然大悟,我们的代码遵循的是“LookBeforeYouLeap”(先看后跳)原则,但实际上我们应该遵循“GetForgivenessiseasiertopermission”(EasiertoAskForgiveness,ThanPermission,也就是不检查,如果有问题会通过异常处理来处理),因为后者更简洁优雅。让我们用一个try-except块重构代码:colors=["brown","red","green","yellow","yellow","brown","brown","black"]color_counts={}forcincolors:try:color_counts[c]=color_counts[c]+1exceptKeyError:color_counts[c]=1现在我们的代码尝试增加每种颜色的计数。如果字典中没有颜色,则抛出KeyError,然后我们将该颜色的计数设置为1。get方法1998年1月1日,我们已升级到Python1.5。我们决定重构之前的代码,使用字典中新增的get方法。colors=[“brown”,“red”,“green”,“yellow”,“yellow”,“brown”,“brown”,“black”]color_counts={}forcincolors:color_counts[c]=color_counts.get(c,0)+1现在我们的代码遍历每种颜色,从字典中获取该颜色的当前计数值。如果没有这个计数值,颜色的计数值默认为0,然后在该值的基础上加1。***将字典中相应键的值设置为新的计数。将主要代码放在一行上很酷,但我们不完全确定它是否更简洁或更优雅。我们认为我们可能有点太聪明了,所以我们还是取消了这次重构。setdefault方法2001年1月1日,我们现在使用的是Python2.0。我们听说字典类型现在有一个setdefault方法,因此决定重构我们的代码以利用它。我们还决定使用新添加的+=增量赋值运算符。colors=["brown","red","green","yellow","yellow","brown","brown","black"]color_counts={}forcincolors:color_counts.setdefault(c,0)color_counts[c]+=1无论是否需要,我们都会通过循环每次调用setdefault方法。但这样做确实使代码看起来更具可读性。我们发现这种方法比以前的代码更加简洁和优雅,所以我们提交了这次修改。fromkeys方法2004年1月1日,我们使用的是Python2.3。我们听说字典增加了一个新的类方法fromkeys(类方法),它可以使用列表中的元素作为键来构建字典。我们重构代码以使用新方法:colors=["brown","red","green","yellow","yellow","brown","brown","black"]color_counts=dict.fromkeys(colors,0)forcincolors:color_counts[c]+=1这段代码创建了一个新的字典,以不同的颜色作为键,每个键的值默认设置为0。这样,当我们增加每个键的值时,我们不必担心它是否已经设置。我们也不需要在代码中进行检查或异常处理,这似乎是一种改进。我们决定修改代码。ComprehensionandSets2005年1月1日,我们现在使用Python2.4。我们发现我们可以使用集合(在Python2.3中发布,在2.4中内置类型)和列表理解(在Python2.0中发布)来解决计数问题。再想一想,发现Python2.4也发布了生成器表达式,最后决定不使用列表推导,而是使用生成器表达式。colors=["brown","red","green","yellow","yellow","brown","brown","black"]color_counts=dict((c,colors.count(c))为cinset(colors))注意:我们在这里没有使用字典理解,因为直到Python2.7才发明字典理解。运行成功,只有一行代码。但是这样的代码够简洁优雅吗?我们想起了Python之禅,这是一个Python编程指南,它起源于Python邮件列表,并悄悄地进入了Python2.2.1。我们在REPL(read-eval-printloop,interactiveinterpreter)界面输入importthis:>>>importthisTheZenofPython,byTimPeters美胜于丑。#Beautifulisbetterthanugly明确胜于含蓄。#清晰优于隐含简单优于复杂。#简单胜于复杂复杂胜于复杂。#复杂胜于难懂扁平胜于嵌套。#扁平优于嵌套稀疏优于密集。#Sparse优于Dense可读性很重要。#易读且有价值特殊情况不足以打破规则。#例外情况不能特别到违反规则的程度。虽然实用胜过纯粹。#尽管实用胜过纯粹。错误不应该悄无声息地过去。#永远的错误不应该在沉默中溜走,除非明确沉默。#除非明确沉默面对歧义,拒绝猜测的诱惑。#面对不确定性,应该有一种——最好只有一种——显而易见的方法来做到这一点。#应该有一种——而不是只有一种——显而易见的方法来做到这一点。尽管除非您是荷兰人,否则这种方式一开始可能并不明显。#也许这种方式一开始并不明显,除非你是荷兰人现在总比没有好。#Nowdoitisbetterthannotdoit虽然从来没有比现在更好。#虽然不做通常比马上做要好Iftheimplement很难解释,这是个坏主意。#如果实现很容易解释,这可能是个好主意。#如果实现很容易解释,这可能是个好主意。#如果实现很容易解释,那可能是个坏主意社区我们的代码变得更复杂了,时间复杂度从O(n)增加到O(n2);它也变得更加丑陋和可读性差。我们这样改是一个有趣的尝试,但是一行代码的实现形式没有我们之前的方法那么简洁优雅。我们已经决定撤消修改。defaultdict方法2007年1月1日,我们使用的是Python2.5。我们刚刚发现defaultdict已添加到标准库中。这样,我们就可以将字典的默认键值设置为0。让我们重构代码以使用defaultdict:fromcollectionsimportdefaultdictcolors=["brown","red","green","yellow","yellow","brown","brown","black"]color_counts=defaultdict(int)forcincolors:color_counts[c]+=1for循环现在变得如此简单!这样绝对更简洁优雅。我们发现color_counts变量的行为现在有点不同,但它确实继承了字典的特性,支持所有相同的映射函数。>>>color_countsdefaultdict(,{'brown':3,'yellow':2,'green':1,'black':1,'red':1})我们没有把color_counts放在这里转换为字典,但假设其他代码也使用了ducktyping(ducktyping,Python中的一种动态类型,这里的意思是:其他代码会把color_counts当作一个字典类型),不再改变这个类似字典的对象。Counter类2011年1月1日,我们使用的是Python2.7。有人告诉我们,使用defaultdict编写的代码不再是计算颜色出现次数的最简洁优雅的方式。Python2.7新引入了一个Counter类,可以彻底解决我们的问题。fromcollectionsimportCountercolors=["brown","red","green","yellow","yellow","brown","brown","black"]color_counts=Counter(colors)比这个方法还要简单?这一定是最简洁优雅的实现了。与defaultdict一样,Counter类返回一个类似字典的对象(实际上是字典的子类)。这足以满足我们的需求,所以我们接受了它。>>>color_countsCounter({'brown':3,'yellow':2,'green':1,'black':1,'red':1})性能方面的考虑注意写这些实现的时候,我们没有注意付给效率问题。大多数方法的时间复杂度是相同的(O(n)),但在不同的Python语言实现(如CPython、PyPy或Jython)下运行时间会有所不同。虽然性能不是我们主要关心的,但我测试了CPython3.5.0实现下的运行时间。由此,你会发现一个有趣的现象:随着列表中颜色元素的密度(即相同元素的个数)发生变化,每种实现方式的相对效率也会有所不同。结论根据Python之禅,“应该有一个——而不应该只有一个明显的方法来做到这一点”。这种说法的状态值得追求,但事实是,并不总是只有一种明显的方式。这种“显而易见”的方法会随着时间、需求和专业水平不断变化。“简洁优雅”(即Pythonic)也是一个相对的概念。