为什么在Python中进行功能编程?

Pau*_*ndt 35 python functional-programming

在工作中,我们习惯以非常标准的OO方式编写Python.最近,有几个人加入了这个功能性的潮流.他们的代码现在包含更多的lambda,map和reduce.我知道函数式语言对并发性有好处,但Python函数编程真的有助于并发吗?我只是想了解如果我开始使用更多Python的功能,我会得到什么.

Ale*_*lli 69

编辑:我已被带到评论的任务(部分似乎,由Python的狂热分子,但不是唯一的)没有提供更多的解释/示例,因此,扩大答案提供一些.

lambda,更是这样map(和filter),最特别的是reduce,在几乎没有在Python中,这是一个强烈的多范式语言工作的工具.

lambda主要优点(?)与普通def语句相比,它是一个匿名函数,同时def给函数一个名字 - 而且为了这个非常可疑的优势,你付出了巨大的代价(函数的主体仅限于一个表达式,结果函数)对象不是pickleable,缺乏名称有时会使得理解堆栈跟踪或以其他方式调试问题变得更加困难 - 我需要继续吗?! - ).

考虑一下你有时会在"Python"中看到的最愚蠢的习惯用法(带有"恐慌引用"的Python),因为它显然不是惯用的Python - 它是来自惯用方案等的错误音译,就像更频繁的过度使用Python中的OOP是来自Java之类的错误音译):

inc = lambda x: x + 1
Run Code Online (Sandbox Code Playgroud)

通过将lambda指定为名称,这种方法立即抛弃了上述"优势" - 并且不会失去任何缺点!例如,inc知道它的名字 - inc.__name__是无用的字符串'<lambda>'- 祝你好运了解堆栈跟踪中的一些;-).在这个简单的例子中,正确的Python方法是实现所需的语义,当然:

def inc(x): return x + 1
Run Code Online (Sandbox Code Playgroud)

现在 inc.__name__是字符串'inc',因为它显然应该是,并且对象是可拾取的 - 语义在其他方面是相同的(在这个简单的情况下,所需的功能在一个简单的表达式中舒适地适合 - def如果你需要,它也很容易重构)临时或永久插入诸如printraise当然的陈述.

lambda是(一部分)表达式,def而是(一部分)一个语句 - 这是人们lambda有时使用的一点语法糖.许多FP爱好者(就像许多OOP和程序粉丝一样)不喜欢Python在表达式和语句之间的相当强烈的区别(对命令查询分离的一般立场的一部分).我,我认为当你使用一种语言时,你最好"使用谷物" - 它的设计方式- 而不是反对它; 所以我用Pythonic方式编程Python,以Schematic(;-)方式构建Scheme,以Fortesque(?)方式编译Fortran,依此类推:-).

继续reduce- 一条评论声称这reduce是计算列表产品的最佳方式.真的吗?让我们来看看...:

$ python -mtimeit -s'L=range(12,52)' 'reduce(lambda x,y: x*y, L, 1)'
100000 loops, best of 3: 18.3 usec per loop
$ python -mtimeit -s'L=range(12,52)' 'p=1' 'for x in L: p*=x'
100000 loops, best of 3: 10.5 usec per loop
Run Code Online (Sandbox Code Playgroud)

所以简单,基本,平凡的循环比执行任务的"最佳方式"快两倍(以及更简洁)? - 我认为速度和简洁的优点必须使得琐碎的循环成为"最好的" "方式,对吧? - )

通过进一步牺牲紧凑性和可读性......:

$ python -mtimeit -s'import operator; L=range(12,52)' 'reduce(operator.mul, L, 1)'
100000 loops, best of 3: 10.7 usec per loop
Run Code Online (Sandbox Code Playgroud)

...我们几乎可以回到最容易获得的最简单,最简单,最简单,最易读的方法(简单,基本,简单的循环).这指出了另一个问题lambda,实际上:性能!对于足够简单的操作,例如乘法,与正在执行的实际操作相比,函数调用的开销非常大 - reduce(mapfilter)经常强制您在简单循环,列表推导和生成器表达式中插入这样的函数调用,允许在线操作的可读性,紧凑性和速度.

也许甚至比上面指出的"将一个lambda指定给一个名字"更糟糕的反成语实际上是以下的反成语,例如按照它们的长度对字符串列表进行排序:

thelist.sort(key=lambda s: len(s))
Run Code Online (Sandbox Code Playgroud)

而不是明显的,可读的,紧凑的,更快速的

thelist.sort(key=len)
Run Code Online (Sandbox Code Playgroud)

在这里,使用lambda除了插入间接级别之外什么都不做 - 没有任何好的效果,还有很多不好的方法.

使用的动机lambda通常是允许使用mapfilter代替非常优选的循环或列表理解,这样可以让你在线进行简单,正常的计算; 当然,你仍然支付"间接水平".这不是Pythonic不得不怀疑"我应该在这里使用listcomp还是map":只要总是使用listcomps,当两者都适用时你不知道选择哪一个,基于"应该有一个,并且最好只做一件明显的做事方式".你经常会编写无法合理地转换为映射的listcomp(嵌套循环,if子句等),而没有调用map它不能被合理地重写为listcomp.

Python中完全正确的函数方法通常包括列表推导,生成器表达式,itertools高阶函数,各种伪装的一阶函数,闭包,生成器(偶尔还有其他类型的迭代器).

itertools作为评论者指出的那样,不包括imapifilter:所不同的是,像所有itertools,这些是基于流的(像mapfilter在Python 3建宏,但是从在Python 2那些建宏不同). itertools提供了一组构建模块,彼此构成良好,并且表现出色:特别是如果你发现自己可能处理很长(甚至无限制的! - )序列,你应该熟悉itertools - 他们的整体文档中的章节有助于阅读,特别是食谱非常有启发性.

编写自己的高阶函数通常很有用,特别是当它们适合用作装饰器时(两个函数装饰器,如文档部分所述,以及Python 2.6中引入的类装饰器).请记住在函数装饰器上使用functools.wraps(以保持函数的元数据被包装)!

所以,总结...:任何你可以使用代码lambda,map以及filter,你可以(比不了最好更多)的代码def(称为函数)和listcomps -通常一个缺口向上移动到发电机,发电机表达式,或者itertools是甚至更好. reduce符合"有吸引力的烦扰"的法律定义......:它几乎不是适合这项工作的工具(这就是为什么它不再是Python 3中的内置功能,最后! - ).

  • 这是非常好的规范性教条.你不应该.很旧的遗嘱,十诫风格的东西.我碰巧大多赞同你(虽然我认为减少是一个红头发的继子女).你能解释一下原因吗? (5认同)
  • @just,是的,你可以在长篇评论中继续咆哮,怯懦地拒绝提供**你自己的**首选反应 - 除了"嘴巴起泡"之外,你还有什么神经回路保持活跃吗?我展示的例子远非被设计,而是经常发生的例子 - 只需检查Stack Overflow以确认这一点.至于错误和它们的发现,简单,清晰,透明的代码(没有复杂和"聪明"的旋转,功能或其他)是第一道防线 - 这意味着使用每种语言,因为它的_designed_被使用... Javascript包括;-). (4认同)
  • 亚历克斯,你能把你的论点整理成标题吗?我发现很难遵循.我知道你不喜欢与`lambda`一起使用的实现限制(我将其解释为"FP在Python中不能很好地支持"),但为什么要反对eta扩展的"\ s.len s",这对任何语言都是坏消息? (4认同)
  • @rvirding,"为你做的事情是自然而然的",即使它不自然和做作*你正在使用的语言*将在未来"回报",当一些可怜的灵魂负责理解和维护它时 - 这么疯狂论证将证明"用任何语言编写Fortran"(或者s/Fortran /你不喜欢的任何其他语言/! - ); 正如有人努力使用FP语言_should_教自己递归,lambdas,模式匹配等,所以有人努力使用_different_语言应该同样使用**语言的自然方式! (3认同)
  • 是的 - 我正在考虑减少不内置,并且响应太快.无论如何 - 也投票.不是因为我是FP one-wayinst,而是因为你是反FP的单向主义者.我更喜欢那些以更平衡的方式回应的人的答案.我认为你的大多数例子都很奇怪,而不是我写的东西,尽管我经常使用FP风格.我使用`reduce`来提高(!)可读性. (3认同)
  • 我认为你的帖子是不平衡的,80%是批评滥用Python的FP工具.我的意思是那种排序更高,感谢抓住它.(但它无论如何都不会改变我的论点 - 在两种情况下它都是高阶函数的使用,并且两种版本都不比另一种更"功能".) (3认同)
  • -1禁止没有解释.`map`,`filter`和`reduce`**是**高阶函数.`itertools`有`imap`和`ifilter`,那怎么更好?你也不应该抨击它吗?list comprehension是`map`和`filter`的语法糖,它不会轻易地用于封装...... python也降低了`apply`以支持`*`,这很糟糕(你可以传递函数,但不能句法糖)... (2认同)
  • 你的编辑添加了一些人为的(/愚蠢的代码)示例.好的,所以你展示了lambda产生了未命名的函数,对于那些使用大量没有任何单元测试的人来说这一定是恐怖的(javascript程序员如何设法在这样的环境中生活?哦,恐怖!).另外,您表明python中的函数式编程会增加函数调用的开销,同时减少与状态相关的错误的机会.`reduce(operators.mul,...)`示例不包含任何变量,并且**与您首选的低级实现一样快*!我不能再发一次你的帖子了.:( (2认同)
  • 恭喜所有组织良好的团队的哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇Cong - - - - - 疼 - 好! - ). (2认同)
  • 你对"lambda"的批评是正确的.但问题是关于Python中的函数式编程.显然你可以在不使用lambda的情况下以函数式编程.从概念上讲,命名函数和匿名函数(你的`inc`示例)之间没有*差别 - 它不会使代码或多或少地起作用.你说`sort(key = lambda s):len(s)`错了,`sort(key = len)`是正确的.这是真的,但"两个"方法都传递了一个更高阶的函数.(CNTD) (2认同)
  • @sdcvvc,我反复提到FP中适合Python的部分(包括HOF,但实际上并没有像你在调用`.sort(key ---)中声称的那样传递HOF! - ) - 但是OP明确地谈到了"很多lambdas,map和reduce",所以如果没有指出那些Python特性的特定问题会非常糟糕(尽管不如说你认为是一个很棒的帖子那么糟糕) "因为你认为另一个更好 - 这不是SO应该如何工作! - ) (2认同)
  • 请注意,map(str,l)比[str(x)for x in l]快得多,因为你实际上必须通过处理函数调用在列表推导中添加一个间接级别 (2认同)

jus*_*ody 24

FP不仅对并发很重要; 事实上,在规范的Python实现中几乎没有并发性(可能3.x会改变吗?).在任何情况下,FP都很适合并发,因为它导致程序没有或没有(显式)状态.由于一些原因,国家很麻烦.一个是他们分配计算硬(呃)(这是并发论证),另一个,在大多数情况下更重要的是,是一种造成错误的倾向.当代软件中最大的漏洞来源是变量(变量与状态之间存在密切关系).FP可能会减少程序中变量的数量:被压扁的错误!

通过在这些版本中混合变量,您可以看到有多少错误:

def imperative(seq):
    p = 1
    for x in seq:
        p *= x
    return p
Run Code Online (Sandbox Code Playgroud)

与(警告,my.reduce参数列表不同于python的参数列表reduce;后面给出的理由)

import operator as ops

def functional(seq):
    return my.reduce(ops.mul, 1, seq)
Run Code Online (Sandbox Code Playgroud)

正如你所看到的那样,事实上FP可以让你更少有机会用变量相关的bug来射击自己.

另外,可读性:它可能需要一些训练,但functional比阅读更容易imperative:你看reduce("好吧,它将序列减少到单个值"),mul("通过乘法").wherease imperative具有for循环的通用形式,充满变量和赋值.这些for周期看起来都是一样的,所以为了了解发生了什么imperative,你需要阅读几乎全部内容.

然后是谦逊和灵活.你给我imperative,我告诉你我喜欢它,但也想要一些东西来总结序列.没问题,你说,然后你走了,复制粘贴:

def imperative(seq):
    p = 1
    for x in seq:
        p *= x
    return p

def imperative2(seq):
    p = 0
    for x in seq:
        p += x
    return p
Run Code Online (Sandbox Code Playgroud)

你能做些什么来减少重复?好吧,如果运营商是价值观,你可以做类似的事情

def reduce(op, seq, init):
    rv = init
    for x in seq:
        rv = op(rv, x)
    return rv

def imperative(seq):
    return reduce(*, 1, seq)

def imperative2(seq):
    return reduce(+, 0, seq)
Run Code Online (Sandbox Code Playgroud)

等一下!operators提供了运营商值!但是...... Alex Martelli reduce已经谴责......看起来如果你想留在他建议的范围内,你注定要复制粘贴的管道代码.

FP版本更好吗?你当然还需要复制粘贴吗?

import operator as ops

def functional(seq):
    return my.reduce(ops.mul, 1, seq)

def functional2(seq):
    return my.reduce(ops.add, 0, seq)
Run Code Online (Sandbox Code Playgroud)

好吧,这只是半成品方法的神器!放弃命令def,你可以将两个版本合同

import functools as func, operator as ops

functional  = func.partial(my.reduce, ops.mul, 1)
functional2 = func.partial(my.reduce, ops.add, 0)
Run Code Online (Sandbox Code Playgroud)

甚至

import functools as func, operator as ops

reducer = func.partial(func.partial, my.reduce)
functional  = reducer(ops.mul, 1)
functional2 = reducer(ops.add, 0)
Run Code Online (Sandbox Code Playgroud)

(func.partial是的原因my.reduce)

运行速度怎么样?是的,在像Python这样的语言中使用FP会产生一些开销.在这里,我只是鹦鹉几位教授对此有何评论:

  • 过早优化是万恶之源.
  • 大多数程序花费80%的运行时间占其代码的20%.
  • 个人资料,不要推测!

我不太擅长解释事情.不要让我把水弄得太多,请阅读John Backus在1977年获得图灵奖时发表的演讲的前半部分.引用:

5.1内在产品的von Neumann程序

c := 0
for i := I step 1 until n do
   c := c + a[i] * b[i]
Run Code Online (Sandbox Code Playgroud)

该程序的几个属性值得注意:

  1. 它的陈述根据复杂的规则在一个看不见的"状态"上运作.
  2. 它不是等级的.除了赋值语句的右侧,它不会从更简单的实体构造复杂的实体.(但是,较大的程序经常这样做.)
  3. 它是动态和重复的.一个人必须在心理上执行它才能理解它.
  4. 它通过重复(赋值)和修改(变量i)来计算一次一个字.
  5. 部分数据,n在程序中; 因此它缺乏一般性,只适用于长度的矢量n.
  6. 它命名其论点; 它只能用于矢量ab.为了变得普遍,它需要一个程序声明.这些涉及复杂问题(例如,逐个呼叫与按值呼叫).
  7. 它的"管家"操作由分散的位置(在for语句和赋值中的下标)中的符号表示.这使得无法将最常见的内务管理操作整合为单一,强大,广泛有用的运营商.因此,在编程这些操作时,必须始终在第一个方向开始,写" for i := ..."和" for j := ...",然后是分配语句,其中包含 i's和j's.

  • *轻笑*这是一个很好的论据,说明一个更完整和彻底的形式,我的前提是当你有一堆变换和组合做而且不需要副作用时,FP非常有用.副作用(也就是状态变化)会使您的程序更难理解,因为您的上一个小块就是很好的例子.`import itertools; a = create_A_list(); b = create_B_list(); c = reduce(lambda c,ab:c +(ab [0]*ab [1]),itertools.izip(a,b),0)`实际上更容易理解,更不容易出错. (2认同)

Ben*_*den 19

我每天都在用Python编程,我不得不说对OO或功能过多的"徘徊"会导致缺少优雅的解决方案.我相信这两种范式对某些问题都有其优势 - 我认为当你知道使用什么方法时.在为您提供干净,可读且高效的解决方案时,请使用功能性方法.OO也是如此.

这就是我喜欢Python的原因之一 - 事实上它是多范式的,让开发人员选择如何解决他/她的问题.


Omn*_*ous 15

这个答案完全重复了.它结合了其他答案的许多观察结果.

正如您所看到的,在Python中使用函数式编程结构存在许多强烈的感受.这里有三大类想法.

首先,几乎每个人,除了那些最坚持最纯粹的功能范式表达的人都同意列表和生成器理解比使用map或更好更清晰filter.你的同事,应避免使用mapfilter如果你的目标一个Python版本足够的新支持列表内涵.你应该避免itertools.imap,itertools.ifilter如果你的Python版本足够新的生成器理解.

其次,整个社会都存在许多矛盾心理lambda.除了def声明函数之外,很多人都对语法感到恼火,特别是涉及像lambda这样的关键字有一个相当奇怪的名字.人们也很恼火,这些小的匿名函数缺少描述任何其他类型函数的任何好的元数据.它使调试更难.最后,声明的小函数lambda通常不是非常有效,因为它们每次调用时都需要Python函数调用的开销,这通常在内部循环中.

最后,大多数人(意思是> 50%,但很可能不是90%)人们认为这reduce有点奇怪和模糊.我自己承认print reduce.__doc__每当我想要使用它时,我都不会经常这样做.虽然当我看到它被使用时,参数的性质(即函数,列表或迭代器,标量)不言而喻.

至于我自己,我陷入了那些认为功能风格通常非常有用的人的阵营.但平衡这种想法是因为Python本身并不是一种功能性语言.过度使用功能结构可能会使程序看起来奇怪扭曲,人们难以理解.

要了解函数样式何时何地非常有用并提高可读性,请在C++中考虑此函数:

unsigned int factorial(unsigned int x)
{
    int fact = 1;
    for (int i = 2; i <= n; ++i) {
        fact *= i;
    }
    return fact
 }
Run Code Online (Sandbox Code Playgroud)

这个循环看起来非常简单易懂.在这种情况下它是.但它看似简单是一个疏忽的陷阱.考虑这种编写循环的替代方法:

unsigned int factorial(unsigned int n)
{
    int fact = 1;
    for (int i = 2; i <= n; i += 2) {
        fact *= i--;
    }
     return fact;
 }
Run Code Online (Sandbox Code Playgroud)

突然,循环控制变量不再以明显的方式变化.您可以减少查看代码并仔细推理循环控制变量的情况.现在这个例子有点病态,但有现实世界的例子没有.问题在于这个想法是重复分配给现有变量的事实.你不能相信变量的值在整个循环体中是相同的.

这是一个长期被认可的问题,在Python中编写这样的循环是相当不自然的.你必须使用while循环,它看起来不对.相反,在Python中你会写这样的东西:

def factorial(n):
    fact = 1
    for i in xrange(2, n):
        fact = fact * i;
    return fact
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,您在Python中讨论循环控制变量的方式不适合在循环中欺骗它.这消除了其他命令式语言中"聪明"循环的许多问题.不幸的是,这是一个半功能语言的想法.

即使这样也适合奇怪的摆弄.例如,这个循环:

c = 1
for i in xrange(0, min(len(a), len(b))):
    c = c * (a[i] + b[i])
    if i < len(a):
        a[i + 1] = a[a + 1] + 1
Run Code Online (Sandbox Code Playgroud)

哎呀,我们又有一个难以理解的循环.它表面上看起来像一个非常简单明显的循环,你必须仔细阅读它才能意识到循环计算中使用的变量之一正在以一种影响循环未来运行的方式被搞乱.

再次,一个更实用的救援方法:

from itertools import izip
c = 1
for ai, bi in izip(a, b):
   c = c * (ai + bi)
Run Code Online (Sandbox Code Playgroud)

现在通过查看代码,我们有一些强烈的指示(部分是由于这个人正在使用这种功能样式),在执行循环期间不会修改列表a和b.少想一想.

最后要担心的是c以奇怪的方式进行修改.也许它是一个全局变量,正在通过一些环形函数调用进行修改.为了拯救我们免受这种心理上的担忧,这里有一个纯粹的功能方法:

from itertools import izip
c = reduce(lambda x, ab: x * (ab[0] + ab[1]), izip(a, b), 1)
Run Code Online (Sandbox Code Playgroud)

非常简洁,结构告诉我们x纯粹是一个累加器.它出现在任何地方都是一个局部变量.最终结果明确地分配给c.现在不用担心了.代码的结构删除了几类可能的错误.

这就是人们可能选择功能风格的原因.它简洁明了,至少如果您了解什么reducelambda做什么.有很多类问题可能会影响以更迫切的风格编写的程序,您知道这些程序不会影响您的功能样式程序.

在阶乘的情况下,有一种非常简单明了的方法可以在函数风格中用Python编写这个函数:

import operator
def factorial(n):
    return reduce(operator.mul, xrange(2, n+1), 1)
Run Code Online (Sandbox Code Playgroud)

  • +1我非常高兴有人通过对比来实际*解释*,哪些方面可能被认为对于合适的问题在功能性和命令式方法中是有益的.我是否遵循所有结论并不重要,重要的是它可以让我做出明智的决定.如果更多的"功能性人"会走这条路 - 而不仅仅是声称*事物 - 我相信FP会在平凡的编程世界中获得更多的采用. (6认同)

Cha*_*art 9

这个问题似乎在这里被忽略了:

编程Python功能真的有助于并发吗?

不会.FP对并发性的价值在于消除计算中的状态,这最终导致并发计算中意外错误的难以掌握的肮脏.但这取决于并发编程习语本身不是有状态的,不适用于Twisted.如果有利用无状态编程的Python的并发习惯用法,我不知道它们.

  • +1用于解决真正的问题,而不是直接跳到元讨论中 (3认同)

sdc*_*vvc 7

以下是关于何时/为何进行功能编程的正面答案的简短摘要.

  • 列表推导是从FP语言Haskell导入的.他们是Pythonic.我更愿意写
y = [i*2 for i in k if i % 3 == 0]
Run Code Online (Sandbox Code Playgroud)

而不是使用命令式构造(循环).

  • 我会lambda在给出一个复杂的钥匙时使用sort,比如list.sort(key=lambda x: x.value.estimate())

  • 使用高阶函数比使用OOP的设计模式(如访问者或抽象工厂)编写代码更清晰

  • 人们说你应该用Python编写Python,用C++编写C++等.这是真的,但你当然应该能够以不同的方式思考同一件事.如果在编写循环时你知道你正在减少(折叠),那么你将能够在更高层次上思考.这可以清理你的思想,并有助于组织.当然,低级思维也很重要.

你不应该过度使用这些功能 - 有许多陷阱,请参阅Alex Martelli的帖子.我主观上说最严重的危险是过度使用这些功能会破坏代码的可读性,这是Python的核心属性.