为什么要在Python中进行function编程?

在工作中,我们用一种非常标准的OO方式来编写我们的Python。 最近,有几个人join了function性的潮stream。 他们的代码现在包含更多的lambdaexpression式,地图和缩减。 我知道函数式语言对并发是有好处的,但是编程Python在function上确实有助于并发? 我只是想了解如果我开始使用更多的Pythonfunction特性,我会得到什么。

编辑 :我已经采取任务的意见(部分,似乎,在Python的FP,但不是唯一的),因为不提供更多的解释/例子,所以扩大答案供应一些。

lambda ,甚至更多地map (和filter ),特别是reduce ,几乎不是Python中工作的正确工具,它是一种强大的多范式语言。

lambda主要优点(?)与正常的def语句相比,它使得一个匿名函数,而def给这个函数一个名字 – 对于这个非常可疑的优势,你付出了巨大的代价(函数的主体仅限于一个expression式,由此产生的函数对象是不可pickleable,非常缺乏名称有时会使得更难以理解一个堆栈跟踪或debugging一个问题 – 需要我继续?! – )。

考虑一下你可能看到的有时候在Python中使用的最愚蠢的成语(Python带有“恐吓引语”,因为它显然不是习惯用法的Python–它是从惯用的Scheme或类似的音译中的一个不好的音译,就像更频繁的过度使用Python中的OOP是来自Java等的不好的音译):

 inc = lambda x: x + 1 

通过将lambda分配给一个名字,这种方法立即抛弃了上述的“优势” – 并且不会丢失任何的缺点! 例如, inc知道它的名字 – inc.__name__是无用的string'<lambda>' – 很好的理解了一些堆栈跟踪;-)。 当然,在这种简单的情况下,用合适的Python方法来实现所需的语义是:

 def inc(x): return x + 1 

现在, inc.__name__是string'inc' ,因为它显然应该是,而对象是pickleable – 语义在其他方面是相同的(在这种简单的情况下,所需的function适合舒适地在一个简单的expression式 – def也使它如果您需要临时或永久插入语句,例如printraise语句,那么可以轻而易举地重构)。

lambda是一个expression式的一部分,而def是一个语句的一部分 – 这是语法糖有时使用lambda 。 许多FP爱好者(就像许多OOP和程序迷一样)不喜欢Python在expression式和语句之间进行合理的强烈区分(对于命令查询分离的一般立场的一部分)。 我认为,当你使用一种语言时,你最好用“用粮食” – 它devise的方式来使用 – 而不是与之抗争; 所以我以Pythonic的方式编程Python,Scheme以Schematic(;-)方式编程,Fortran以Fortesque(?)方式编写,等等:-)。

继续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 

所以简单的,基本的,微不足道的循环比执行任务的“最好的方式”快两倍(以及更简洁) – 我认为速度和简洁的优点必须使平凡的循环成为“最好的循环” “方式,对吗? – )

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

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

…我们几乎可以回到最简单,最明显,紧凑和可读的方法(简单,基本,平凡的循环)容易获得的性能。 这指出了lambda另一个问题,实际上:性能! 对于相当简单的操作,例如乘法,函数调用的开销与正在执行的实际操作相比是非常重要的 – reduce (以及mapfilter )通常会强制您插入这样的函数调用,其中简单循环,列表parsing,和发生器expression式,使得在线操作的可读性,紧凑性和速度成为可能。

也许比上面所说的更糟糕的是,“将一个lambda赋值给一个名称”的反成语实际上是下面的反成语,例如按照它们的长度来sorting一个string列表:

 thelist.sort(key=lambda s: len(s)) 

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

 thelist.sort(key=len) 

在这里, lambda的使用除了插入一个间接的级别,没有什么好的效果,还有很多坏的。

使用lambda的动机通常是允许使用mapfilter而不是一个非常可取的循环或列表理解,这样可以让你在行中进行普通的正常计算; 当然,你仍然支付那个“间接”的水平。 不得不想知道“我应该在这里使用一个listcomp还是一个映射表”,而不是Pythonic:只要总是使用listcomps,当两个表单都适用时,你不知道应该select哪一个,最好只有一个,明显的做法“。 你经常写listcomps,不能被合理地翻译成地图(嵌套循环, if子句等),而没有调用map ,不能被合理地重写为listcomp。

Python中完美适用的函数式方法通常包括列表itertools ,生成器expression式, itertools ,高阶函数,各种伪装,闭包,生成器(偶尔还有其他types的迭代器)的一阶函数。

itertools ,作为一个评论者指出,包括imapifilter :不同之处在于,像所有itertools一样,这些都是基于stream的(比如Python 3中的mapfilter builtins,但是与Python 2中的builtins不同)。 itertools提供了一系列相互组合的构build块,并且performance出色:特别是如果你发现自己有可能处理很长(甚至是无界)的序列,那么你应该对自己熟悉itertools – 它们整篇文章使得阅读更好,而食谱尤其具有启发性。

编写自己的高阶函数通常是有用的,特别是当它们适合用作装饰器 (包括函数装饰器,如文档中的部分解释和Python 2.6中介绍的类装饰器)。 请记住在你的函数装饰器上使用functools.wraps (保持函数的元数据被包装)!

所以,总结…:任何你可以用lambdamapfilter编码的东西,你可以用def (named functions)和listcomps来编写代码(通常比不好) ,或itertools ,甚至更好。 reduce符合“吸引人的滋扰”的法律定义…:它几乎不是工作的正确工具(这就是为什么它不再是Python 3中的内置,终于!)。

FP不仅对并发很重要, 实际上,规范的Python实现中几乎没有并发(可能是3.x的变化?)。 在任何情况下,FP都很适合并发,因为它会导致不具有或更less(明确)状态的程序。 州有麻烦的原因有几个。 一个是他们使得计算很难(er)(这是并发性的争论),另一个在大多数情况下更重要的是造成错误的倾向。 当代软件中最大的错误来源是variables (variables和状态之间有密切的关系)。 FP可能会减less一个程序中的variables数量:bug被压扁了!

通过在这些版本中混合variables,可以看到有多less错误可以引入:

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

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

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

正如你所看到的,事实上,FP给了你更less的机会,用一个variables相关的bug在自己的脚上射击。

另外,可读性:可能需要一些培训,但functional方式比阅读方式更容易阅读:你看到reduce (“好吧,这是一个序列减less到一个单一的值”), mul (“通过乘法”)。 在哪里imperative具有循环的一般forms,充满variables和任务。 这些循环都看起来是一样的,所以要了解当前的情况,你需要阅读几乎所有的内容。

那么就有了灵活性和灵活性。 你给我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 

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

 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) 

等一下! operators为运营operators提供了价值! 但是……亚历克斯·马尔泰利(Alex Martelli)已经谴责已经reduce了…看起来像是如果你想保持在他所build议的界限之内,你注定要复制pipe道密码。

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) 

那么,这只是一个半分析方法的人造物! 放弃强制性的def ,你可以将两个版本合同

 import functools as func, operator as ops functional = func.partial(my.reduce, ops.mul, 1) functional2 = func.partial(my.reduce, ops.add, 0) 

甚至

 import functools as func, operator as ops reducer = func.partial(func.partial, my.reduce) functional = reducer(ops.mul, 1) functional2 = reducer(ops.add, 0) 

func.partialfunc.partial的原因)

运行速度怎么样? 是的,在像Python这样的语言中使用FP会招致一些开销。 在这里,我只鹦鹉教授几个教授要说的这个:

  • 过早的优化是万恶之源。
  • 大多数程序在其运行时间的80%中花费了20%的代码。
  • 简介,不要揣测!

我不擅长解释事情。 不要让我浑水太多,请阅读约翰·巴克斯(John Backus)在1977年获得图灵奖(Turing Award)的演讲前半部分。Quote:

5.1内部产品的冯·诺依曼程序

 c := 0 for i := I step 1 until n do c := c + a[i] * b[i] 

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

  1. 它的陈述根据复杂的规则在一个无形的“状态”上运作。
  2. 这不是分层的。 除了赋值语句的右边,它不会构造更简单的复杂实体。 (然而,更大的程序往往是这样。)
  3. 这是dynamic的和重复的。 必须在精神上执行它来理解它。
  4. 它通过重复(赋值)和修改(variablesi)来逐字计算。
  5. 部分数据n在程序中; 因此它缺乏通用性,只适用于长度为n向量。
  6. 它命名它的论点; 它只能用于vectorab 。 要成为一般的,它需要一个程序声明。 这些涉及复杂的问题(例如,按名称与按价值划分)。
  7. 它的“家务”操作在分散的地方用符号表示(在for语句和在任务中的下标)。 这使得把最常见的家务pipe理整合到单一的,强大的,广泛使用的操作员中是不可能的。 因此,在对这些操作进行编程时,必须始终从第一个方块开始,写下“ for i := ... ”和“ for j := ... ”,然后是赋值语句,后面撒上ij

我每天都用Python进行编程,而且我不得不说太多的OO或者function上的“混搭”可能导致缺less优雅的解决scheme。 我相信这两种模式都有一定的优势,而且我认为当你知道使用什么方法的时候。 当它为您提供一个干净,可读,高效的解决scheme时,使用function性的方法。 OO也一样。

这就是我喜欢Python的原因之一 – 这是一个多范式的事实,可以让开发者select如何解决他/她的问题。

这个答案完全重新工作。 它包含了很多来自其他答案的观察结果。

正如你所看到的,围绕在Python中使用函数式编程结构有很多强烈的感受。 这里有三个主要的想法组。

首先,除了那些最贴近function范式最纯粹expression的人之外,几乎每个人都认为列表和发生器的理解比使用mapfilter更好,更清晰。 你的同事应该避免使用mapfilter如果你的目标是足够新的Python版本支持列表parsing。 你应该避免itertools.imapitertools.ifilter如果你的Python的版本是足够新的发电机综合。

其次,整个社会对lambda有很多的矛盾心理。 除了用于声明函数的def之外,很多人还真的被一个语法困扰,特别是涉及像lambda这样的关键字的名字相当陌生。 而且人们也很恼火,这些小的匿名函数缺less描述任何其他types函数的好元数据。 这使得debugging更加困难。 最后,由lambda声明的小函数通常不是非常有效,因为每次调用时都需要Python函数调用的开销,这通常在内部循环中。

最后,大多数人(意味着> 50%,但很可能不是90%)认为reduce有点奇怪和模糊。 我自己承认print reduce.__doc__每当我想要使用它,这是不常见的。 虽然当我看到它使用时,参数的性质(即函数,列表或迭代器,标量)自己说话。

至于我自己,我倒在那些认为function风格通常很有用的人的阵营里。 但是平衡这个想法是事实上,Python不是一个function语言。 过度使用function结构会使程序看起来很奇怪地被扭曲,很难让人理解。

要理解函数风格何时何地非常有用,并提高可读性,请在C ++中考虑这个函数:

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

这个循环看起来非常简单易懂。 而在这种情况下呢。 但它看似简单却是一个陷阱。 考虑写这个循环的另一种方法:

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

突然间,循环控制variables不再以明显的方式变化。 你只需仔细查看代码和推理,看看循环控制variables会发生什么。 现在这个例子有点病态,但是现实世界的例子却不是。 问题在于这个想法是重复赋值给一个现有的variables。 你不能相信variables的值在整个循环体中是相同的。

这是一个长期以来公认的问题,在Python中编写这样的循环相当不自然。 你必须使用while循环,而且看起来不对。 相反,在Python中你可以这样写:

 def factorial(n): fact = 1 for i in xrange(2, n): fact = fact * i; return fact 

正如你所看到的那样,你在Python中讨论循环控制variables的方式并不适合在循环中与其混淆。 这消除了其他命令式语言中“巧妙”循环的许多问题。 不幸的是,这是一个从function语言中借用的想法。

即使这样也会使自己陷入奇怪的摆弄。 例如,这个循环:

 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 

哎呀,我们又有一个难以理解的循环。 它表面上类似于一个非常简单和明显的循环,你必须仔细阅读它,以便意识到循环计算中使用的variables之一正在被混淆,将影响将来的循环运行。

再一次,更有效的救援方法:

 from itertools import izip c = 1 for ai, bi in izip(a, b): c = c * (ai + bi) 

现在通过查看代码,我们得到了一些强烈的指示(部分是由于人们正在使用这种function风格),在执行循环期间,列表a和b没有被修改。 还有一件事要考虑。

最后要担心的是被奇怪的修改。 也许这是一个全局variables,正在被一些迂回的函数调用修改。 为了把我们从精神上的烦恼中解救出来,这里是一个纯粹的function方法:

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

非常简洁,结构告诉我们x是纯粹的累加器。 它在任何地方都是一个局部variables。 最后的结果是毫不含糊地分配给c。 现在有更less的担心。 代码的结构删除了几类可能的错误。

这就是为什么人们可能会select一种function风格。 这是简明扼要,至less如果你明白什么reducelambda做。 有大量的问题可能折磨写一个更强制性的风格的程序,你知道不会折磨你的function风格程序。

在factorial的情况下,有一个非常简单明了的方法来在Python中以函数式编写这个函数:

 import operator def factorial(n): return reduce(operator.mul, xrange(2, n+1), 1) 

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

编程Pythonfunction真的帮助并发?

不。FP带来的并发性是消除计算中的状态,最终导致并发计算中的非预期错误的难以掌握。 但是这取决于并发编程习语本身不是有状态的,不适用于Twisted。 如果有利用无状态编程的Python并发习语,我不知道它们。

下面是为什么在function上进行编程时的积极答案的简短总结。

  • 列表parsing是从FP语言Haskell导入的。 他们是Pythonic。 我宁愿写
 y = [i*2 for i in k if i % 3 == 0] 

比使用命令式的结构(循环)。

  • 当给一个复杂的键进行sort ,我会使用lambda ,如list.sort(key=lambda x: x.value.estimate())

  • 使用更高级的函数比使用OOP的访问者或抽象工厂的devise模式编写代码更清洁

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

你不应该过度使用这些function – 有很多陷阱,请参阅Alex Martelli的post。 我主观地说,最严重的危险是过度使用这些特性会破坏你的代码的可读性,这是Python的一个核心属性。

标准函数filter(),map()和reduce()用于列表上的各种操作,并且所有三个函数都需要两个参数:函数和列表

我们可以定义一个单独的函数,并将其用作filter()等的参数,如果函数被多次使用,或者如果函数太复杂而不能写入一行,那么它可能是个好主意。 但是,如果只需要一次,而且非常简单,那么使用lambda构造生成(临时)匿名函数并将其传递给filter()会更方便。

这有助于readability and compact code.

使用这些函数也会变得efficient ,因为循环列表中的元素在C中完成,这比在Python中循环要快一些。

除了抽象,分组等,如果要求非常简单,那么在维护状态的时候强制需要面向对象的方式,而不是面向对象编程。

地图和filter在OO编程中占有一席之地。 就在列表推导和生成器函数旁边。

减less如此。 减lessalgorithm可以迅速吸收更多的时间, 有一点点思考,手动编写的reduce-loop会比使用一个糟糕的循环函数的reduce的效率更高。

Lambda永远不会。 Lambda是无用的。 人们可以说它确实做了一些事情,所以它不是完全无用的 。 第一:Lambda不是句法“糖”; 它使事情变得更大更丑陋。 第二:一万行代码认为你需要一个“匿名”function的代码在两万行代码中变成两次,这就消除了匿名的价值,使其成为维护责任。

然而。

无对象状态变化编程的function风格本质上仍然是OO。 你只是做更多的对象创build和更less的对象。 一旦开始使用发生器function,许多OO编程会在function方向上漂移。

每个状态的变化似乎都转化为一个生成器函数,用于从旧对象构build新状态的新对象。 这是一个有趣的世界观,因为对algorithm的推理要简单得多。

但是这不是使用reduce或lambda的调用。