有时,解决问题的Python方法会随着时间而改变。随着Python的发展,用Python方式计算列表项的方式也是如此。
我们来看一下计算事物在列表中出现的次数的不同技术。在分析这些技术时,我们只会关注代码样式。稍后我们会担心性能。
我们需要一些历史背景来理解这些不同的技术。幸运的是,我们生活在__future__中,并且拥有一台时间机器。让我们跳入DeLorean并进入1997年。
是1997年1月1日,我们正在使用Python 1.4。我们有一个颜色列表,我们很想知道每种颜色在该列表中出现了多少次。让我们使用字典!
颜色= ["棕色" ,"红色" ,"绿色" ,"黄色" ,"黄色" ,"棕色" ,"棕色" ,"黑色" ] color_counts = {}对于颜色c:if color_counts。 has_key(c):color_counts [c] = color_counts [c] + 1 else:color_counts [c] = 1
注意:我们不使用+ =,因为在Python 2.0之前不会添加增强分配,并且我们不会在color_counts惯用语中使用c,因为在Python 2.2之前不会发明c!
运行此命令后,我们将看到我们的color_counts词典现在包含列表中每种颜色的计数:
那很简单。我们只是循环浏览每种颜色,检查它是否在字典中,如果不是,则添加颜色,如果是,则增加计数。
颜色= ["棕色" ,"红色" ,"绿色" ,"黄色" ,"黄色" ,"棕色" ,"棕色" ,"黑色" ] color_counts = {}对于c颜色:如果不是color_counts。 has_key(c):color_counts [c] = 0 color_counts [c] = color_counts [c] + 1
在稀疏列表(具有很多非重复颜色的列表)上,这可能会稍慢一些,因为它执行的是两个语句而不是一个,但是我们并不担心性能,我们担心代码风格。经过一番思考,我们决定继续使用这个新版本。
是1997年1月2日,我们仍在使用Python 1.4。我们今天早上醒来时突然意识到:我们的代码正在练习“跨越式的思考”(LBYL),而我们本应练习“比请求权限更容易宽恕”(EAFP),因为EAFP更像Python。让我们重构代码以使用try-except块:
颜色= ["棕色" ,"红色" ,"绿色" ,"黄色" ,"黄色" ,"棕色" ,"棕色" ,"黑色" ] color_counts = {}对于颜色中的c:try:color_counts [c] = color_counts [c] + 1,但KeyError:color_counts [c] = 1
现在,我们的代码尝试增加每种颜色的计数,如果该颜色不在词典中,则会引发KeyError,而是将颜色的颜色计数设置为1。
1998年1月1日,我们已经升级到Python 1.5。我们决定将代码重构为对字典使用新的get方法:
颜色= ["棕色" ,"红色" ,"绿色" ,"黄色" ,"黄色" ,"棕色" ,"棕色" ,"黑色" ] color_counts = {}对于颜色为c的颜色:color_counts [c] = color_counts。得到(c,0)+ 1
现在,我们的代码循环遍历每种颜色,从字典中获取颜色的当前计数,将该计数默认为0,将计数加1,然后将字典键设置为此新值。
这全是一行代码,这很酷,但是我们不确定这是否是Pythonic。我们认为这可能太聪明了,因此我们恢复了此更改。
是2001年1月1日,我们现在正在使用Python 2.0!我们听说字典现在有一个setdefault方法,我们决定重构代码以使用此新方法。我们还决定使用新的+ =扩充赋值运算符:
颜色= ["棕色" ,"红色" ,"绿色" ,"黄色" ,"黄色" ,"棕色" ,"棕色" ,"黑色" ] color_counts = {}对于颜色为c的颜色:color_counts。 setdefault(c,0)color_counts [c] + = 1
无论是否需要,都会在每个循环中调用setdefault方法,但这似乎更具可读性。我们认为这比以前的解决方案更具Python风格,并做出更改。
是2004年1月1日,我们正在使用Python 2.3。我们听说过字典上有一个新的fromkeys类方法,该方法可从键列表中构造字典。我们将代码重构为使用此新方法:
颜色= ["棕色" ,"红色" ,"绿色" ,"黄色" ,"黄色" ,"棕色" ,"棕色" ,"黑色" ] color_counts = dict。 fromkeys(colors,0)for c的颜色:color_counts [c] + = 1
这将使用我们的颜色作为键创建一个新字典,所有值最初都设置为0。这使我们可以递增每个键,而不必担心它是否已设置。我们不再需要进行任何检查或异常处理,这似乎是一种改进。我们决定保留此更改。
是2005年1月1日,我们正在使用Python 2.4。我们意识到,我们可以使用集(在Python 2.3中发布并在2.4中内置)和列表推导(在Python 2.0中发布)解决计数问题。经过进一步思考,我们记得生成器表达式也是在Python 2.4中发布的,我们决定使用其中一个而不是列表理解:
颜色= ["棕色" ,"红色" ,"绿色" ,"黄色" ,"黄色" ,"棕色" ,"棕色" ,"黑色" ] color_counts = dict((set(colors)中c的((c,colors。count(c))))
注意:我们没有使用字典理解,因为只有在Python 2.7之前,它们才被发明。
我们记得Python的Zen,它始于python-list电子邮件线程中,并被Python 2.2.1所吸引。我们在REPL中输入import this:
>>>蒂姆·彼得斯(Tim Peters Beautiful)导入的《 Python的禅宗》比丑陋更好。显式胜于隐式。简单胜于复杂。复杂胜于复杂。扁平比嵌套更好。稀疏胜于密集。可读性很重要。特殊情况不足以违反规则。尽管实用性胜过纯度。错误绝不能默默传递。除非明确地保持沉默。面对模棱两可的想法,拒绝猜测的诱惑。应该有一种-最好只有一种-显而易见的方法。除非您是荷兰人,否则起初这种方式可能并不明显。现在总比没有好。尽管从来没有比现在“正确”好。如果难以解释该实现,则是个坏主意。如果实现易于解释,则可能是个好主意。命名空间是一个很棒的主意-让我们做更多的事吧!
我们的代码更复杂(用O(n 2)代替O(n)),不太漂亮,可读性更差。所做的更改是一个有趣的实验,但是这种单线解决方案比我们现有的Pythonic少。我们决定还原此更改。
这是2007年1月1日,我们正在使用Python 2.5。我们刚刚发现defaultdict现在在标准库中。这应该允许我们将0设置为字典中的默认值。让我们重构代码以使用defaultdict进行计数:
从集合中导入defaultdict colors = ["棕色" ,"红色" ,"绿色" ,"黄色" ,"黄色" ,"棕色" ,"棕色" ,"黑色" ] color_counts =用于颜色c的defaultdict(int):color_counts [c] + = 1
我们意识到,color_counts变量的行为不同,但是它确实继承自dict并且支持所有相同的映射功能。
我们假设其余的代码实践未将color_counts转换为dict,而是直接进行了鸭式输入,并将此类似dict的对象保持原样。
是2011年1月1日,我们正在使用Python 2.7。有人告诉我们,我们的defaultdict代码不再是计算颜色的最Python方式。 Python 2.7的标准库中包含一个Counter类,它为我们完成了所有工作!
从集合中导入专柜颜色= ["棕色" ,"红色" ,"绿色" ,"黄色" ,"黄色" ,"棕色" ,"棕色" ,"黑色" ] color_counts =计数器(颜色)
像defaultdict一样,它返回一个类似dict的对象(实际上是dict的子类),对于我们的目的来说应该足够好,因此我们会坚持下去。
请注意,我们并未关注这些解决方案的效率。这些解决方案中的大多数具有相同的时间复杂度(大O表示法为O(n)),但运行时可能会因Python实现而异。
尽管性能不是我们的主要关注点,但是我确实测量了CPython 3.5.0的运行时间。有趣的是,每种实现方式如何根据列表中颜色名称的密度改变相对效率。
按照Python Zen的说法,“应该有一种(最好只有一种)明显的方式来做到这一点”。这是一个理想的消息。并非总是有一种明显的方式来做到这一点。 “显而易见”的方式会随时间,需求和专业水平的不同而变化。
导入此内容和Python的Zen:从这篇文章中借来的Python Zen琐事
Python如何:用字典进行分组和计数:在撰写本文时,我发现了这篇相关文章
感谢Brian Schrader和David Lord对本文的校对,并感谢Micah Denbraver在正确版本的Python上实际测试了这些解决方案。