Why program functionally in Python?
在工作中,我们使用相当标准的OO方式对Python进行编程。最近,有几个人加入了功能性的潮流。他们的代码现在包含了更多的lambda、maps和reduce。我知道函数语言对于并发性很好,但是编程python在功能上真的有助于并发性吗?我只是想了解如果我开始使用更多的Python功能特性,我会得到什么。
编辑:在评论中我被带到了任务中(在某种程度上,似乎是被python中fp的狂热爱好者们所吸引,但并非完全如此),因为我没有提供更多的解释/例子,所以,我扩展了答案来提供一些。好的。
想想什么可能是你在"Python"中使用的最愚蠢的习惯用法(Python带有"吓唬引语"),因为它显然不是惯用的Python——它是从惯用方案之类的坏音译,就像在Python中频繁使用OOP是来自Java等的坏音译:好的。
1 | inc = lambda x: x + 1 |
通过将lambda分配给一个名称,这种方法立即放弃了上述的"优势",并且不会失去任何缺点!例如,
1 | def inc(x): return x + 1 |
现在,
继续讨论
1 2 3 4 | $ 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 |
所以简单、基本、琐碎的循环比执行任务的"最佳方式"快两倍(也更简洁)?-)我想速度和简洁的优势一定会使琐碎的循环成为"最好的"方式,对吧?-)好的。
通过进一步牺牲紧凑性和可读性…:好的。
1 2 | $ 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"更糟糕的是,反惯用法实际上是以下反惯用法,例如按字符串的长度对字符串列表进行排序:好的。
1 | thelist.sort(key=lambda s: len(s)) |
而不是显而易见的,可读的,紧凑的,更快的好的。
1 | thelist.sort(key=len) |
在这里,使用
使用
在python中,完全合适的函数方法通常包括列表理解、生成器表达式、
正如一位评论者指出的那样,
编写自己的高阶函数通常很有用,尤其是当它们适合用作装饰器时(文档的这一部分中解释了函数装饰器和Python2.6中介绍的类装饰器)。请记住在函数修饰符上使用functools.wrapps(保持函数的元数据被包装)!好的。
所以,总结一下……:任何可以用
FP不仅对于并发性很重要;事实上,在规范化的Python实现中实际上没有并发性(可能3.x会改变这一点?)在任何情况下,fp都很适合并发,因为它会导致没有或更少(显式)状态的程序。各州之所以麻烦,有几个原因。一个是它们使分配计算变得困难(ER)(这是并发性论点),另一个在大多数情况下更重要的是,它们有造成错误的倾向。当代软件中最大的缺陷源于变量(变量和状态之间有着密切的关系)。fp可以减少程序中变量的数量:bug被压扁了!
通过在这些版本中混合变量,了解可以引入多少错误:
1 2 3 4 5 | def imperative(seq): p = 1 for x in seq: p *= x return p |
与(警告,
1 2 3 4 | import operator as ops def functional(seq): return my.reduce(ops.mul, 1, seq) |
正如你所看到的,事实上,fp给了你更少的机会用一个变量相关的bug射自己的脚。
另外,可读性:它可能需要一些训练,但
还有简洁和灵活。你给我一个
1 2 3 4 5 6 7 8 9 10 11 | 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 |
你能做些什么来减少重复?如果算符是值,你可以做
1 2 3 4 5 6 7 8 9 10 11 | 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) |
哦,等等!
FP版本更好吗?当然你也需要复制粘贴?
1 2 3 4 5 6 7 | import operator as ops def functional(seq): return my.reduce(ops.mul, 1, seq) def functional2(seq): return my.reduce(ops.add, 0, seq) |
好吧,那只是一个半途而废的方法的产物!放弃命令式
1 2 3 4 | import functools as func, operator as ops functional = func.partial(my.reduce, ops.mul, 1) functional2 = func.partial(my.reduce, ops.add, 0) |
甚至
1 2 3 4 5 | import functools as func, operator as ops reducer = func.partial(func.partial, my.reduce) functional = reducer(ops.mul, 1) functional2 = reducer(ops.add, 0) |
(
运行时速度如何?是的,在类似于python的语言中使用fp会产生一些开销。在这里,我将重复一些教授对此的看法:
- 过早的优化是万恶之源。
- 大多数程序将80%的运行时花费在20%的代码中。
- 简介,不要猜测!
我不擅长解释事情。别让我把水搅浑得太多,看看约翰·巴克斯在1977年获得图灵奖时发表的前半段演讲。报价:
5.1 A von Neumann Program for Inner Product
1
2
3 c := 0
for i := I step 1 until n do
c := c + a[i] * b[i]Several properties of this program are
worth noting:Its statements operate on an invisible"state" according to complex
rules.It is not hierarchical. Except for the right side of the assignment
statement, it does not construct
complex entities from simpler ones.
(Larger programs, however, often do.)It is dynamic and repetitive. One must mentally execute it to
understand it.It computes word-at-a-time by repetition (of the assignment) and by
modification (of variable i).Part of the data, n , is in the program; thus it lacks generality and
works only for vectors of lengthn .It names its arguments; it can only be used for vectors a andb .
To become general, it requires a
procedure declaration. These involve
complex issues (e.g., call-by-name
versus call-by-value).Its"housekeeping" operations are represented by symbols in
scattered places (in the for statement
and the subscripts in the assignment).
This makes it impossible to
consolidate housekeeping operations,
the most common of all, into single,
powerful, widely useful operators.
Thus in programming those operations
one must always start again at square
one, writing"for i := ... " and
"for j := ... " followed by
assignment statements sprinkled with
i 's andj 's.
我每天都在用Python编程,我不得不说,太多的面向OO或函数的"随波逐流"可能会导致缺少优雅的解决方案。我相信这两种模式对某些问题都有优势——我认为这就是你知道使用什么方法的时候。当它留给您一个干净、可读和高效的解决方案时,请使用功能性方法。OO也是如此。
这就是我喜欢Python的原因之一——它是多范式的,并且允许开发人员选择如何解决他/她的问题。
这个答案完全是重复的。它包含了许多来自其他答案的观察结果。好的。
正如您所看到的,在使用Python中的函数式编程构造时,有很多强烈的感觉。这里有三大类想法。好的。
首先,除了那些最热衷于功能范式最纯粹表达的人之外,几乎所有人都同意列表和生成器的理解比使用
第二,整个社会对
最后,大多数人(意味着大于50%,但很可能不是90%)认为
至于我自己,我属于那些认为功能风格通常非常有用的人的阵营。但平衡这一思想的事实是,Python本质上不是一种功能性语言。功能结构的过度使用会使程序看起来异常扭曲,让人难以理解。好的。
要理解函数样式在何时何地是非常有用的,并提高可读性,请考虑C++中的这个函数:好的。
1 2 3 4 5 6 7 8 | unsigned int factorial(unsigned int x) { int fact = 1; for (int i = 2; i <= n; ++i) { fact *= i; } return fact } |
这个循环看起来很简单,也很容易理解。在这种情况下是这样的。但它表面上的简单是粗心大意者的陷阱。考虑这种编写循环的替代方法:好的。
1 2 3 4 5 6 7 8 | unsigned int factorial(unsigned int n) { int fact = 1; for (int i = 2; i <= n; i += 2) { fact *= i--; } return fact; } |
突然,回路控制变量不再以明显的方式变化。您只需仔细查看代码,并仔细推理循环控制变量会发生什么情况。现在这个例子有点病态化,但现实世界中有一些例子不是。问题在于,这个想法是对现有变量的重复赋值。不能相信变量的值在整个循环体中是相同的。好的。
这是一个长期公认的问题,在Python中编写这样的循环是相当不自然的。你必须用一个while循环,它看起来是错误的。相反,在python中,您将编写如下内容:好的。
1 2 3 4 5 | def factorial(n): fact = 1 for i in xrange(2, n): fact = fact * i; return fact |
正如您所看到的,在python中谈论循环控制变量的方式不适合在循环中愚弄它。这消除了其他命令式语言中"聪明"循环的许多问题。不幸的是,这是一个半借用功能语言的想法。好的。
即使这样,也会导致奇怪的玩弄。例如,这个循环:好的。
1 2 3 4 5 | 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 |
哎呀,我们又有了一个难以理解的循环。它表面上类似于一个非常简单和明显的循环,您必须仔细阅读它,才能意识到循环计算中使用的一个变量正以一种影响循环未来运行的方式受到干扰。好的。
同样,一种更实用的救援方法:好的。
1 2 3 4 | from itertools import izip c = 1 for ai, bi in izip(a, b): c = c * (ai + bi) |
现在,通过查看代码,我们得到了一些强有力的指示(部分原因是人员正在使用这种功能样式),即在循环执行期间不会修改列表A和B。少想一件事。好的。
最后要担心的是C被以奇怪的方式修改了。也许它是一个全局变量,正在被某个迂回函数调用修改。为了让我们摆脱这种精神上的忧虑,这里有一个纯粹的功能方法:好的。
1 2 | from itertools import izip c = reduce(lambda x, ab: x * (ab[0] + ab[1]), izip(a, b), 1) |
非常简洁,结构告诉我们x纯粹是一个累加器。它在任何地方都是一个局部变量。最后的结果是明确地分配给C的,现在不用担心了。代码的结构删除了几个可能的错误类。好的。
这就是为什么人们会选择一种实用的风格。它简洁明了,至少如果你理解
在factorial的情况下,有一种非常简单和清晰的方法可以用函数样式的python编写这个函数:好的。
1 2 3 | import operator def factorial(n): return reduce(operator.mul, xrange(2, n+1), 1) |
好啊。
这个问题在这里似乎被忽视了:
does programming Python functionally really help with concurrency?
不,fp给并发带来的价值是消除计算中的状态,这最终导致难以理解并发计算中的意外错误。但这取决于并发编程的习惯用法,而不是它们本身是有状态的,这是不适用于Twisted的。如果有针对Python的并发习语可以利用无状态编程,我不知道它们。
以下是一个简短的肯定回答,即何时/为什么进行功能性编程。
- 列表理解是从一种fp语言haskell导入的。他们是Python。我更喜欢写信
1 y = [i*2 for i in k if i % 3 == 0]
而不是使用命令式构造(循环)。
在给
sort 一把复杂的钥匙时,我会使用lambda ,就像list.sort(key=lambda x: x.value.estimate()) 一样。使用高阶函数比使用OOP的设计模式(如访问者或抽象工厂)编写代码更干净
人们说你应该用Python编程Python,C++中的C++等等。这是真的,但是当然你应该能够以不同的方式思考同样的事情。如果在写一个循环的时候你知道你真的在做减少(折叠),那么你就能够在更高的层次上思考。这会清洁你的头脑,有助于组织。当然,低级思维也很重要。
你不应该过度使用这些功能-有很多陷阱,见亚历克斯马泰利的帖子。我主观地说,最严重的危险是过度使用这些特性会破坏代码的可读性,这是Python的核心属性。
标准函数filter()、map()和reduce()用于列表上的各种操作,这三个函数都需要两个参数:函数和列表
我们可以定义一个单独的函数,并将其用作filter()等的参数,如果该函数被多次使用,或者函数太复杂,无法用一行代码编写,这可能是个好主意。但是,如果只需要一次,而且非常简单,那么使用lambda构造生成(临时)匿名函数并将其传递给filter()会更方便。
这对
使用这些函数,结果也将是
当状态需要维护时,除了抽象、分组等之外,强制需要面向对象的方法,如果需求很简单,我会坚持功能性而不是面向对象的编程。
map和filter在OO编程中占有一席之地。在列表理解和生成器函数旁边。
减少,所以。reduce算法可以快速地减少比它所需要的时间更多的时间;通过一点思考,手动编写的reduce循环将比reduce更有效,reduce将一个思路不清的循环函数应用于序列。
永远不会。lambda是无用的。人们可以说它确实做了一些事情,所以它并不是完全无用的。第一:lambda不是语法上的"sugar";它使事物变得更大更丑。第二:在10000行代码中,有一次认为你需要一个"匿名"函数,而在20000行代码中,这一次变成了两次,这就消除了匿名的价值,使之成为维护责任。
然而。
无对象状态变化编程的功能风格仍然是面向对象的。你只需要做更多的对象创建和更少的对象更新。一旦开始使用生成器函数,很多OO编程都会朝着函数的方向漂移。
每个状态更改似乎都转换为生成器函数,该函数从旧对象在新状态下构建新对象。这是一个有趣的世界观,因为对算法的推理要简单得多。
但这不是使用reduce或lambda的调用。