关于函数式编程:为什么在Python中使用函数编程?

Why program functionally in Python?

在工作中,我们使用相当标准的OO方式对Python进行编程。最近,有几个人加入了功能性的潮流。他们的代码现在包含了更多的lambda、maps和reduce。我知道函数语言对于并发性很好,但是编程python在功能上真的有助于并发性吗?我只是想了解如果我开始使用更多的Python功能特性,我会得到什么。


编辑:在评论中我被带到了任务中(在某种程度上,似乎是被python中fp的狂热爱好者们所吸引,但并非完全如此),因为我没有提供更多的解释/例子,所以,我扩展了答案来提供一些。好的。

lambda,甚至更多的是map(和filter),尤其是reduce,在python(一种强大的多范式语言)中,几乎永远都不是合适的工作工具。好的。

lambda主要优势(?)与正常的def语句相比,它是一个匿名函数,而def给函数起了一个名字,而且为了这个非常可疑的优势,你付出了巨大的代价(函数的主体仅限于一个表达式,产生的函数对象是不可处理的,缺少一个名字有时会使它非常困难为了理解堆栈跟踪或调试问题,需要继续吗?!)好的。

想想什么可能是你在"Python"中使用的最愚蠢的习惯用法(Python带有"吓唬引语"),因为它显然不是惯用的Python——它是从惯用方案之类的坏音译,就像在Python中频繁使用OOP是来自Java等的坏音译:好的。

1
inc = lambda x: x + 1

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

1
def inc(x): return x + 1

现在,inc.__name__是字符串'inc',正如它应该的那样,对象是可处理的——语义在其他方面是相同的(在这个简单的例子中,所需的功能很适合于一个简单的表达式——def,如果需要临时或永久地插入状态,那么重构也非常容易。当然,如printraise等)。好的。

lambda是一个表达式的一部分,而def是一个语句的一部分,这是一种语法糖,使人们有时使用lambda。许多FP爱好者(和许多OOP和过程爱好者一样)不喜欢Python在表达式和语句之间相当强的区别(这是命令查询分离的一般立场的一部分)。我,我认为,当你使用一种语言时,最好是用"有纹理的"(它的设计方式)而不是与之抗争;所以我用Python的方式编程,用示意图的方式(;-)设计,用fortsque(?)设计。等等:—)。好的。

继续讨论reduce——一条评论称,reduce是计算列表乘积的最佳方法。哦,真的吗?让我们看看……好的。

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的另一个问题,实际上是:性能!对于足够简单的操作(如乘法),与正在执行的实际操作相比,函数调用的开销是相当大的——reduce(和mapfilter常常强制您插入这样一个函数调用,其中简单的循环、列表理解和生成器表达式允许readab线性操作的可压缩性、紧凑性和速度。好的。

也许比上面所说的"为名称分配lambda"更糟糕的是,反惯用法实际上是以下反惯用法,例如按字符串的长度对字符串列表进行排序:好的。

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

而不是显而易见的,可读的,紧凑的,更快的好的。

1
thelist.sort(key=len)

在这里,使用lambda只不过是插入一个间接的层次——没有任何好的效果,而且有很多坏的效果。好的。

使用lambda的动机通常是允许使用mapfilter,而不是一个非常好的循环或列表理解,这样可以让您进行简单的、正常的计算;当然,您仍然支付"间接水平"。"我应该在这里使用ListComp还是地图?"这不是一个皮索尼克式的疑问:只要总是使用ListComp,当两者看起来都适用并且你不知道要选择哪一个时,基于"应该有一个,最好只有一个,显而易见的方法去做某事"。您通常会编写不能被明智地转换为map的listcomp(嵌套循环、if子句等),而不会调用不能被明智地重写为listcomp的map。好的。

在python中,完全合适的函数方法通常包括列表理解、生成器表达式、itertools、高阶函数、各种形式的一阶函数、闭包、生成器(偶尔还有其他类型的迭代器)。好的。

正如一位评论者指出的那样,itertools的确包括imapifilter:区别在于,与所有的itertools一样,这些都是基于流的(如mapfilter内置在python 3中,但与python 2中的内置不同)。itertools提供了一套相互配合良好的构建块,以及出色的性能:特别是当你发现自己可能要处理很长时间(甚至是无限期的)时。-)顺序,你应该自己熟悉ITertools——他们在文档中的整个章节都有助于阅读,特别是食谱非常有指导意义。好的。

编写自己的高阶函数通常很有用,尤其是当它们适合用作装饰器时(文档的这一部分中解释了函数装饰器和Python2.6中介绍的类装饰器)。请记住在函数修饰符上使用functools.wrapps(保持函数的元数据被包装)!好的。

所以,总结一下……:任何可以用lambdamapfilter编码的代码,都可以用def和listcomps(命名函数)编码(通常比不有利),并且通常向上移动一个等级到生成器、生成器表达式或itertools更好。reduce符合"吸引人的麻烦"的法律定义……:它几乎永远都不是适合工作的工具(这就是为什么它最终不再是Python3中的内置工具的原因!-)好的。好啊。


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

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

1
2
3
4
import operator as ops

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

正如你所看到的,事实上,fp给了你更少的机会用一个变量相关的bug射自己的脚。

另外,可读性:它可能需要一些训练,但functionalimperative更容易阅读:你看到reduce("好,它将一个序列还原为一个值"),mul("乘")。其中,imperative具有for循环的一般形式,充满变量和赋值。这些for周期看起来都是一样的,所以为了了解imperative中发生了什么,你需要阅读几乎所有的内容。

还有简洁和灵活。你给我一个imperative,我告诉你我喜欢它,但也想要一些序列的和。没问题,你说,然后离开,复制粘贴:

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)

哦,等等!operators提供的运算符是值!但是…亚历克斯·马泰利已经谴责了埃多克斯。看起来,如果你想保持在他建议的范围内,你就注定要复制粘贴管道代码。

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)

好吧,那只是一个半途而废的方法的产物!放弃命令式def,您可以将两个版本都承包给

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)

(func.partialmy.reduce的原因)

运行时速度如何?是的,在类似于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 length n.
  • It names its arguments; it can only be used for vectors a and b.
    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 and j's.

  • 我每天都在用Python编程,我不得不说,太多的面向OO或函数的"随波逐流"可能会导致缺少优雅的解决方案。我相信这两种模式对某些问题都有优势——我认为这就是你知道使用什么方法的时候。当它留给您一个干净、可读和高效的解决方案时,请使用功能性方法。OO也是如此。

    这就是我喜欢Python的原因之一——它是多范式的,并且允许开发人员选择如何解决他/她的问题。


    这个答案完全是重复的。它包含了许多来自其他答案的观察结果。好的。

    正如您所看到的,在使用Python中的函数式编程构造时,有很多强烈的感觉。这里有三大类想法。好的。

    首先,除了那些最热衷于功能范式最纯粹表达的人之外,几乎所有人都同意列表和生成器的理解比使用mapfilter更好、更清晰。你的同事应该避免使用mapfilter,如果你的目标是一个足以支持列表理解的python版本。如果您的Python版本足够新,可以理解生成器,那么您应该避免使用itertools.imapitertools.ifilter。好的。

    第二,整个社会对lambda存在着很大的矛盾心理。除了def用于声明函数的语法之外,还有很多人真的很讨厌这种语法,特别是涉及像lambda这样一个名字很奇怪的关键字的语法。人们也很恼火,因为这些小的匿名函数缺少描述任何其他类型函数的元数据。这使得调试更加困难。最后,由lambda声明的小函数通常效率不太高,因为它们需要每次调用python函数时的开销,而python函数调用通常位于内部循环中。好的。

    最后,大多数人(意味着大于50%,但很可能不是90%)认为reduce有点奇怪和模糊。我自己也承认,每当我想使用它的时候,都会有print reduce.__doc__,这并不是经常发生的。不过,当我看到它被使用时,参数的性质(即函数、列表或迭代器、标量)就代表了它们自己。好的。

    至于我自己,我属于那些认为功能风格通常非常有用的人的阵营。但平衡这一思想的事实是,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的,现在不用担心了。代码的结构删除了几个可能的错误类。好的。

    这就是为什么人们会选择一种实用的风格。它简洁明了,至少如果你理解reducelambda的作用。有大量的问题可能影响以命令式方式编写的程序,而您知道这不会影响您的功能式程序。好的。

    在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()会更方便。

    这对readability and compact code.有帮助

    使用这些函数,结果也将是efficient,因为列表元素上的循环是在C中完成的,这比Python中的循环要快一点。

    当状态需要维护时,除了抽象、分组等之外,强制需要面向对象的方法,如果需求很简单,我会坚持功能性而不是面向对象的编程。


    map和filter在OO编程中占有一席之地。在列表理解和生成器函数旁边。

    减少,所以。reduce算法可以快速地减少比它所需要的时间更多的时间;通过一点思考,手动编写的reduce循环将比reduce更有效,reduce将一个思路不清的循环函数应用于序列。

    永远不会。lambda是无用的。人们可以说它确实做了一些事情,所以它并不是完全无用的。第一:lambda不是语法上的"sugar";它使事物变得更大更丑。第二:在10000行代码中,有一次认为你需要一个"匿名"函数,而在20000行代码中,这一次变成了两次,这就消除了匿名的价值,使之成为维护责任。

    然而。

    无对象状态变化编程的功能风格仍然是面向对象的。你只需要做更多的对象创建和更少的对象更新。一旦开始使用生成器函数,很多OO编程都会朝着函数的方向漂移。

    每个状态更改似乎都转换为生成器函数,该函数从旧对象在新状态下构建新对象。这是一个有趣的世界观,因为对算法的推理要简单得多。

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