我从许多 Python 专家那里听说,他们更喜欢列表推导,因为他们可以使用高阶函数(例如 filter 和 reduce 等)完成所有您可以做的事情。所以这个问题解决了他们:什么是你可以用它们做的事情的一个可靠的例子,这对 HOF 来说是棘手的?
6 回答
答案是没有这样的例子。你可以用列表推导做的所有事情都会机械地翻译成高阶函数。事实上,这就是 Haskell 实现列表推导的方式:它将它们脱糖为高阶函数。
给定这样的列表理解:
[(x, y) | x <- [1..3], y <- [4..6]]
Haskell 将其脱糖:
concatMap (\x -> concatMap (\y -> [(x, y)]) [4..6]) [1..3]
同样,如果您输入以下谓词:
[(x, y) | x <- [1..3], y <- [4..6], x + y /= 5]
......然后,这意味着:
concatMap (\x -> concatMap (\y -> if (x + y) == 5 then [(x, y)] else []) [4..6]) [1..3]
事实上,这种脱糖是 Haskell 规范的一部分,您可以在此处找到。
如前所述,你可以用列表推导式做的所有事情都可以去糖化为高阶函数,但是在 Python 中这样做的很大一部分问题是 Python 缺乏对你可以使用的那种无点编程的支持。filter
, map
, 和 Haskell 中的朋友。这是一个有点人为的例子,但我想你会明白的。
让我们看一下这个 Python 代码:
[(x,y) for x,y in zip(xrange(20), xrange(20, 0, -1)) if x % 2 == 0 and y % 2 == 0]
它所做的只是打印出来:
[(0, 20), (2, 18), (4, 16), (6, 14), (8, 12), (10, 10), (12, 8), (14, 6), (16, 4), (18, 2)]
这是带有过滤器的等效版本:
filter(lambda ns : ns[0] % 2 == 0 and ns[1] % 2 == 0, zip(xrange(20), xrange(20, 0, -1)))
我希望你会同意我的看法,那就是它更丑陋。如果不定义一个单独的函数,你真的无法做很多事情来使它不那么难看。
但是让我们看一下 Haskell 中的等效版本:
[(x,y) | (x,y) <- zip [0..20] [20,19..0], x `mod` 2 == 0 && y `mod` 2 == 0]
好的,和 Python 列表理解版本一样好。那么等效的过滤器版本呢?
import Data.Function
let f = (&&) `on` (==0) . (`mod` 2)
filter (uncurry f) $ zip [0..20] [20,19..0]
好的,我们必须进行导入,但是一旦您了解了它的作用,代码(imo)就会更加清晰,尽管有些人可能仍然喜欢f
被指向,甚至是带有过滤器的 lambda。在我看来,无点版本更简洁,概念上更清晰。但我想说的主要一点是,它在 Python 中并没有那么清楚,因为在不引入单独的库的情况下无法部分应用函数,并且缺少组合运算符,所以在Python中它是一个最好选择列表推导而不是映射/过滤器,但在 Haskell 中,它可以根据具体问题采取任何一种方式。
在 Haskell 中,列表推导是条件和函数的“语法糖”(或者可以简单地翻译成 do 表示法,然后单子去糖)。这是翻译它们的“官方”指南:http ://www.haskell.org/onlinereport/haskell2010/haskellch3.html#x8-420003.11
因此,由于列表推导式可以使用简单的高阶函数机械而直接地转换为等效代码,因此根据定义,没有它们你就不能用它们做任何事情。
其他都是正确的;与 map、reduce、filter 等函数相比,列表推导本身并没有提供任何更好的序列操作。但是,它们并没有真正解决您的问题,即为什么 Python 程序员胜过列表推导而不是高阶函数。
Python 提倡它并且 Python 程序员使用它们的原因是因为根据 Guido 的说法,语言创建者、列表推导(以及集合推导和 dict 压缩和生成器表达式)比函数表达式更容易阅读和编写。Python 的哲学是可读性胜过一切。
Guido 一般不喜欢函数式编程结构,并且对添加lambda
语法持谨慎态度。这只是风格和品味的问题,而不是表现力或力量。他的观点塑造了 Python 及其编写方式。
有关更多详细信息,这里是 Guido 提出的从 Python 3 及更高版本中删除lambda
、map
、filter
和的建议。reduce
它没有实现(除了删除reduce
,不再是内置函数),但他在这里列出了他的推理:http ://www.artima.com/weblogs/viewpost.jsp?thread=98196
不过,他总结如下:
filter(P, S) 几乎总是更清楚地写成 [x for x in S if P(x)],这具有巨大的优势,即最常见的用法涉及比较谓词,例如 x==42,并定义一个 lambda 只需要读者付出更多的努力(加上 lambda 比列表理解慢)。
相比
[[x*x, x*x+x ..] | x <- [2..]]
和
map (\x-> map (*x) $ enumFrom x) $ enumFrom 2
第一个显然更具可读性。您问的是“棘手”,而不是“不可能”。使用filter
,没有任何迹象表明我们是在过滤还是过滤出通过或失败的给定测试的元素。对于 LC,它在视觉上很明显。
因此,只要有 LC 配方,它就是 IMO 的首选,只是为了它的可读性。Haskell 的 LC 语法特别简洁明了,比 Python 的 IMO 更清晰(噪音更小)。不好意思不用。:)
出于这个问题的确切原因,我很少使用列表推导。但是,在一种情况下,我发现它们是唯一简洁的语法:当可反驳的模式位于<-
. 例子:
data Foo = Bar Int | Baz String
getBazs :: [Foo] -> [String]
getBazs xs = [x | Baz x <- xs]
要在没有列表理解的情况下编写它,您必须像这样做更长的事情:
data Foo = Bar Int | Baz String
getBazs :: [Foo] -> [String]
getBazs = foldr go []
where go (Baz x) acc = x:acc
go _ acc = acc
但与列表推导不同的是,这不是一个“好的生产者”,因此它的输出列表将无法与任何东西融合。要解决这个问题,您必须手动添加重写规则,或者切换到导入一个好的生产者的不同函数:
import Data.Maybe
data Foo = Bar Int | Baz String
getBazs :: [Foo] -> [String]
getBazs = mapMaybe go
where go (Baz x) = Just x
go _ = Nothing
最终结果是,对于相同的结果,它比基本的列表理解要考虑更多,代码也更多。