原文:Idiomatic Python: comprehensions
很幸运,我们组有几个人已经用Python编程了相当长的一段时间(我自己现在已经使用这门语言超过15年了)。随着时间的推移,我们纷纷使用各种各样Python编程中的惯用手法,这些惯用手法可能不是很明显,或者处于各种原因并未广为人知。为了帮助大家分享一些这方面的知识,我们计划进行题为“地道的Python”的不定期的博客文章,在这里,我们选择一个主题,并提供关于它的一些小技巧。有些文章可能很长,有些则可能很短。无论哪种方式,我们都希望,你能从中得到一些有用的东西(即使是对于经验丰富的开发人员而言,因为我们可能会在这里那里讲到一点Python历史)。
对于该系列的首篇文字,我想我们可以谈谈推导。在几乎所有的编程语言中,填充一个容器的一个惯用手法就是使用for
循环:
container = []
for x in another_container:
container.append(x**2)
这是用一些对象填充诸如列表的容器的一种非常简单直接的方式。但是,这种惯用手法相当常见,以至于一遍又一遍的重复变成有点讨厌。这就是为什么在Python 2.0中,列表推导被添加到了Python中:
container = [x**2 for x in another_container]
(小历史:Python中的列表推导是受Haskell中的列表推导所启发的)。列表推导已被广泛用于Python社区,因为它们提供了一种以紧凑的方式来从另一个容器中创建一个列表的简单的方式(技术上来讲,是从另一个可迭代对象,但是每种类型的容器都应该是一个可迭代对象)。得益于Python在背后暗戳戳做的一些事,列表推导也快于一个简单的for
循环。
但你可能不知道的是,Python中还有生成器、字典和集合的推导版本。在Python 2.4中,生成器表达式被添加到了Python中。当你想通过不在内存中一次性创建一个完整的列表来节省内存,而只是想要使用需要的内存来一次创建序列中的一个项时(谁不想节省内存呢?),生成器和列表推导、生成器表达式的交叉使用是很棒的:
container = (x**2 for x in another_container)
就像列表推导,生成器表达式或多或少是下面的语法糖:
def _():
for x in another_container:
yield x**2
container = _()
del _
要真正了解这一点,你可以分别尝试下面的两个例子,但要注意,后者将需要一段时间才能完成:
# Use xrange() instead of range() if you're using Python 2.7.
# Generator expression
('genexp' for _ in range(100000000))
# List comprehension
['listcomp' for _ in range(100000000)]
每当你使用列表推导的时候,你应该停下来想想结果列表将如何被使用。如果该列表只是被当做一个可迭代对象 — 列表中的所有对象将被依次访问,正如在for
循环中一样 — 那么你应该使用一个生成器表达式即可。
这也过渡到了这个惯用手法:在有可能的时候,指定你的API接受或返回一个可迭代对象,而不是一个指定的类型,例如一个列表。当你只关心一个对象序列时,指定你的API接受/返回可迭代对象,而不是一个具体的类型,例如一个列表。这一点与列表推导如何在Python 3中工作的很好的联系在一起,因为基本上生成器表达式加上对list()
的调用,意味着返回一个可迭代对象,例如生成器表达式,并不会阻碍你的代码的用户轻松的获得一个列表,如果列表就是他们想要的话:
container = list(x**2 for x in another_container)
列表推导和生成器表达式变得如此受欢迎,以至于Python继续发展推导的概念,并在Python 3.0中引入了字典和集合推导 (它们被反向移植到Python 2.7)。顾名思义,字典和集合推导提供了从for
循环创建字典和集合的语法糖。集合推导看起来像是一个生成器表达式,但它使用的是花括号:
container = {x**2 for x in another_container}
字典推导看起来像集合推导,但字典中的每一项使用冒号来分开键值,就像在字典中一样:
container = {x: x**2 for x in another_container}
现在,你可能已经注意到了在所有Python内置的容器类型中,只有元组没有推导形式。这是故意的,因为推导并不意味着只是for
循环的语法糖。由于你不能在一个for
循环中创建一个元组,因此元组的推导形式是没有意义的。同时,很容易根据一个生成器表达式来创建一个元组,因此这并没有丢失掉任何功能,并且与使用for
循环来创建一个列表,然后最后将其转换成一个元组相比,并没有任何性能损失:
container = tuple(x**2 for x in another_container)
总结一下:
- 列表推导是受Haskell所启发的:
[x**2 for x in another_container]
- 生成器表达式就像列表推导一样,但它用于生成器:
(x**2 for x in another_container)
- 你应该总是尝试使用生成器表达式,而不是列表推导,并且只在你确实需要一个列表的时候使用列表推导
- 当你只关心对象序列时,指定你的API接受/返回可迭代对象,而不是具体的类型,例如,列表。
- Python 2.7/3.0添加了集合推导:
{x**2 for x in another_container}
- Python 2.7/3.0还添加了字典推导:
{x: x**2 for x in another_container}
希望你觉得这篇文章有用!如果是的话,请告诉我们,以便我们能够衡量是否应该在未来写更多像这样的文章。(Ele注:请拉到文章开头,找到原文地址,去反馈吧!)