33 评论

为啥俺推荐 Python[4]:作为函数式编程语言的 Python

  春节前看到热心读者留言,提醒俺:Python 系列好久没更新了(不知不觉又过了一年多)。俺当时回复说:春节假期补上后一篇。昨天听到鞭炮声才发觉元宵已经到了,赶忙写出本文。
  前一个帖子介绍了 Python 作为“面向对象编程”(以下简称 OOP)语言的特点,今天来聊一聊 Python 作为“函数式编程”(以下简称 FP)语言的特点。考虑到本系列面向的是 Python 的门外汉或刚入门的新手,故本文仅介绍若干浅显的 FP 特性。

★什么是函数式编程


  说实话,“函数式编程语言”是一个很大的话题。由于篇幅有限,本文不可能对这个话题做全面介绍。俺干脆偷一下懒,只简单说说。
  从字面上看,所谓的函数式编程,就是以“函数”为中心的“编程范式”。估计有同学又会问了,啥是“编程范式”捏?哎呦,这又是一个很大的话题。通俗来讲,“编程范式”就是指编程的套路。比方说大家很熟悉的 OOP,就是一种“编程范式”。FP 跟 OOP 一样,都是一种编程的套路。做个简单类比:OOP 以“对象/类”作为程序设计的核心,而 FP 以“函数”作为程序设计的核心。

★FP的特点


  既然说到 FP,自然要稍微说一下 FP 的特色。

◇函数很牛X


  刚才说了,FP 是以函数为中心。既然如此,在支持 FP 的语言中,函数的功能自然十分牛X。通俗地说,OOP 语言中,类/对象能干的事情,FP 语言中的函数也能干。下面做一些对比,以加深大伙儿的印象。
  OOP 中,对象可以互相赋值;FP 中,函数可以互相赋值。
  OOP 中,对象可以作为函数的参数/返回值,FP 中,函数可以作为函数的参数/返回值。
  某些 OOP 中,类可以嵌套定义;FP 中,函数可以嵌套定义。
  某些 OOP 中,可以有匿名类;FP 中,可以有匿名函数。

◇避免副作用


  在 FP 中,特别强调函数不要有“副作用”(洋文叫“side effect”)。没有副作用的函数,又称之为纯函数(pure function)。其输出完全依赖于输入。换句话说,只要输入一样,输出就一样。
  要成为纯函数,函数内部不能读写函数外部变量、不能有设备 I/O(比如读写文件)......
  无副作用是 FP 的重要特性。FP的很多优点来自于此特性。

◇避免控制流


  在 FP 中,尽量避免用控制流语句(循环语句、判断语句)。对于控制流语句,FP 有另外的替代方式。比如:常用递归或高阶函数来替代循环。如此一来,FP 的代码会显得更简洁,更可读。

◇多态


  大部分支持 FP 的语言,也都支持多态。函数参数支持多态化,便可实现非常灵活的功能。

  说了这么多,不知道大伙儿明白了没?还是没整明白的同学,请看维基百科的英文词条(中文词条太简单,看不明白滴)。
  洋文实在看不下去吗?那不妨看看 IT 大牛 Joel 写的《你的编程语言能这样做吗?》(中文版在“这里”)。此文以 JavaScript 来阐述 FP 的妙处。

★FP 的优点


  再稍微说一下 FP 的好处,以强化大伙儿学习的动力。

◇模块化


  在 FP 的思想中,函数最好是“纯”的,而且最好只完成“单一”的任务。在这种指导思想下,函数的模块化程度自然就高。

◇可复用性


  模块化程度高,直接的好处就是可复用性好。

◇可读性


  刚才说了,FP 的思想强调函数又纯又小。这样的函数,代码的可读性自然好,修改起来也方便。

◇易于调试


  前面提到了纯函数。如果你的程序中大部分函数都是纯函数,则调试 Bug 会容易很多。像 OOP 中,类的多个成员函数都可以修改类的成员变量,有时候会导致调试极其困难。而纯函数没有此问题。
  另外,多线程是调试的一大噩梦。当年俺还专门写过帖子,介绍 C++ 多线程的注意事项(在“这里”)。而纯函数由于没有副作用,不必担心各种“互斥”、“死锁”等问题。

◇易于测试


  除了易于调试,纯函数的输出仅仅依赖于输入,这一特点注定了它很容易进行单元测试。

◇适合并行


  在 FP 中,由于纯函数无副作用,很适合编写并行处理的代码。最典型并且在工业界获得巨大成功的例子就是 Erlang。

◇其它


  当然啦,FP 的好处远不止上述这些(比如还有:利于形式化证明)。限于篇幅,俺就不展开了。

★Python 的函数语法


  Python 中,常见的函数定义和函数调用,想必各位都晓得了。下面说几种不太常见的,且跟 FP 有关的语法。

◇函数赋值


  Python 可以把函数直接赋值给一个变量。举例如下:
def square(n) :  # 这是一个计算平方的小函数,后面会反复用它举例
    return n ** 2

f = square  # 此处赋值给变量 f
f(10)  # 此处返回100。注意:对该变量使用小括号,等同于调用函数

◇匿名定义


  Python 可以用 lambda 关键字定义【单行】的匿名函数。套用刚才的例子
square = lambda x : x**2  # 定义一个单参数的匿名函数,并把该函数赋值给变量
square(10)  # 此处返回 100

◇嵌套定义


Python 支持函数的嵌套定义(请看如下例子)。这种语法,在“闭包”中经常出现(后面会具体介绍“闭包”)。
def outer() :  # 外层函数
    s = "hello"
    def inner() :  # 内层函数
        print(s)  # 此处引用的是外层作用域的变量

    inner()  # 输出 hello
    s = "world"
    inner()  # 输出 world


★和 FP 相关的内置函数


  Python 内置了一大坨用于 FP 的函数,以方便程序猿写出简洁的代码。在接下去聊之前,俺有必要先介绍其中的2个。

◇map(func, iter)


  为了省事,俺只介绍2参数的 map(正宗的 map 支持 N 参数)。
  参数 func 是个函数,参数 iter 是个迭代器(也可以理解为集合)
  map() 会把 iter 的每个元素传给 func,并把每次调用的结果保存到一个 list 中,然后返回此 list。
  举例:
  挨个计算整数 list 的平方:
map(square, [1, 2, 3])  # 返回 [1, 4, 9]

◇filter(func, iter)


  参数含义同 map
  filter() 会把 iter 的每个元素传给 func,如果 func 返回结果为 True,就把元素保存在一个 list 中,最后返回此 list。
  举例:
  要过滤出所有奇数,代码如下:
def odd(n) :
    return (n%2) == 1

filter(odd, [1, 2, 3])  # 返回[1, 3]

  此处可以用上 lambda,把代码简化为一行:
filter(lambda n: (n%2)==1, lst)


★消除控制流


  为了让大伙儿更深刻体会 FP 风格同传统风格的差别,俺把刚才两个例子组合一下——要求返回整数 list 中所有奇数的平方。
  传统的写法(有控制流):
def func1(old_lst) :
    new_lst = []
    for n in old_lst :
        if odd(n) :
            new_lst.append(square(n))
    return new_lst

  FP 的写法(无控制流):
def func2(lst) :
    return map(square, filter(odd, lst))

  怎么样?是不是更简洁?连 for / if 这两个关键字都不需要了。


★List Comprehension


  这个洋文比较难翻译。有人叫做“列表推导”,也有人称为“列表展开”或“列表解析”。(俺比较喜欢头一个翻译——不禁让人联想到“推倒”:)
  在 Python 中,这是一个很好吃的语法糖——可以让你写出很简洁、很优雅的代码。
  举例1:
  还拿刚才过滤奇数的例子。
filter(lambda n: (n%2)==1, lst)

  上述写法可以等价替换为列表推导:
[n for n in lst if (n%2)==1]

  举例2:
  再来一个稍微复杂的例子。假设有两个整数 list,分别存储矩形的宽度和高度。现在想把所有的宽度和高度进行两两组合,把大于 10 的面积打印出来。
  传统的写法(2层循环,4行代码)
for w in width :
    for h in height :
        if w*h > 10 :
            print(w*h)

  FP 的写法(无循环,1行代码,多精致啊)
print( [w*h for w in width for h in height if w*h > 10] )

  除了列表推导,Python 中还有字典推导、集合推导等等。为了省点口水,暂且打住。


★闭包


  闭包,洋文叫“closure”,解释在“这里”。它是 FP 的常见手法。那闭包到底有啥用捏?俺举一个微积分中,函数求导的例子。(不懂微积分或者对高数有心理阴影的同学,别担心,请把注意力集中在代码上)
def d(f) :
    def calc(x) :
        dx = 0.000001  # 表示无穷小的Δx
        return (f(x+dx) - f(x)) / dx  # 计算斜率。注意,此处引用了外层作用域的变量 f
    return calc  # 此处用函数作为返回值(也就是函数 f 的导数)

现在,假设要计算二次函数 f(x) = x2 + x + 1 的导数,只需如下代码:
f = lambda x : x**2 + x + 1  # 先把二次函数用代码表达出来
f1 = d(f)  # 这个f1 就是 f 的一阶导数啦。注意,导数依然是个函数

  有了一阶导数,就可以很容易地计算该函数在某点的斜率
  比如要计算 x=3 的斜率,只需:
f1(3)

  如果要想得到二阶导数(导数的导数),只需依样画葫芦(瞧这代码写得多优雅)
f2 = d(f1)

  看到这里,大伙儿不妨设想一下:如果不用 FP,改用 OOP,上述需求该如何实现?俺觉得吧,用 OOP 来求导,这代码写起来多半是又丑又臭。

★结尾


  今天聊了不少 FP 的语法特性,可惜还是没聊完。由于俺比较懒,而且怕写得太长没人看,所以一些高级话题(比如:迭代器、生成器、等),今天就不介绍了。假如列位看官对那些玩意儿感兴趣,再抽空单独写一帖。

回到本系列的目录
版权声明
本博客所有的原创文章,作者皆保留版权。转载必须包含本声明,保持本文完整,并以超链接形式注明作者编程随想和本文原始地址:
https://program-think.blogspot.com/2012/02/why-choose-python-4-fp.html

33 条评论

  1. 嵌套定义处的样例有些小问题吧?那个n是不是该是s?

    回复删除
  2. 正在入门,很有帮助,3Q。
    期待下一篇!

    回复删除
  3. to ushuz
    非常感谢提醒,俺已经修正了笔误。

    回复删除
  4. 王立军黄海刺胡事件http://xz6.2000y.com/mb/1/readnews.asp?newsid=661134

    回复删除
  5. python 的程序确实很简洁,正在学

    回复删除
  6. 每次看博主的文章都颇有收获,真心话

    回复删除
  7. 初学python,python确实是一门很方便的语言,需要什么就直接调用,模块库十分丰富,我目前学的是python for s60。不过我总感觉没有学到什么,如果脱离模块,什么也不会,我担心这样下去,能力会不会退化。

    回复删除
  8. to 楼上的网友
    Python作为一门容易上手的语言,确实挺适合初学者。
    如果你希望在编程方面有更深入的进展,有几种不同的方式。具体取决于你的目标以及你的现状。

    咱们不妨邮件沟通一下。

    回复删除
  9. 博主,我在考虑,能否使用写微分函数类似的方法写出某函数的不定积分函数呢?

    回复删除
  10. TO xr
    老实说,不定积分函数是咋回事,俺已经有点忘了 :(
    应该也可以用“闭包”的方式写出来。

    回复删除
  11. 博主,推荐个Python IDE吧! 这个系列希望继续写下去!

    回复删除
  12. TO Goodspeed
    非常抱歉 :(
    最近一段时间,精力都花在写政治类博文。
    俺抽空把本系列的下一篇准备一下。

    回复删除
  13. 如果你希望在编程方面有更深入的进展,有几种不同的方式。具体取决于你的目标以及你的现状。
    坐等博客分享,或者邮件告知。ljia38091@gmail.com 谢谢

    回复删除
    回复
    1. 你可以给俺写信(博客右上方有俺的联系方式),说说你希望在哪些方面有提高,然后俺再针对性回复。

      删除
  14. 找到了个好地方,政治类资料着实令人大开眼界,还是个同行。期待更新。

    回复删除
    回复
    1. 多谢 IT 同行捧场 :)
      俺会抽空把 Python 系列补上。

      删除
  15. 好看 继续更新这个系列啊

    回复删除
    回复
    1. TO Unknown
      俺会抽空把 Python 系列补上。

      删除
  16. 感觉列表推导和C#里面的linq很像

    回复删除
    回复
    1. TO limian
      当初设计 C# 的时候,有借鉴 Python 的语法。
      而 Python 的很多语法,其实是借鉴了 Lisp

      删除
  17. 谷歌“质数算法”,第一条进了这个博客。之后顺其自然又多看到了不少感兴趣的内容,看完这篇决定回复一下博主以表感谢,希望多更新~

    回复删除
  18. 不玩编程20年了,现在看这些东西,还是觉得很亲切。当年我编程的时候就梦想有语言能支持运行时能通过学习改变自身运行逻辑,现在看来Python越出了一大步。准备学学教儿子。

    回复删除
    回复
    1. 多谢老程序员捧场 :)
      Python 语言本身,是非常适合于初学者的,尤其是不懂 IT 技术的初学者。
      同时,在程序高手眼里,Python 依然是利器。

      删除
  19. 偶然发现这个博客,竟是同行,多谢楼主的分享,受益良多,期待楼主再接再厉!

    回复删除
  20. python 的 map, filter, reduce 真是相当的方便。python是目前使用过的脚本语言中最优秀的一个。boost里面function的很多功能也很类似。不知道是谁学习了谁。python的语法简洁, 真是没话说。

    >>> list(map(lambda x: x ** 2, [1, 2, 3]))
    [1, 4, 9]

    >>> list(map(pow, [2, 3, 4], [10, 11, 12]))
    [1024, 177147, 16777216]

    >>> list(filter(lambda x: x %2 != 0, [1, 2, 3]))
    [1, 3]

    >>>from functools import reduce
    >>> reduce(lambda x, y: x + y, [1, 2, 3, 4])
    10
    >> reduce(lambda x, y: x * y, [1, 2, 3, 4])
    24

    回复删除
  21. 楼主啥时候能把python的坑继续填完呀?

    还有楼主能否抽空谢谢boost系列的, 这个对于c++程序员来说可是很重要的东西呀

    回复删除
    回复
    1. 谢谢 =》 写写

      居然留言无法修改

      删除
    2. TO JMGCD
      多谢提醒 :)
      其实这个 Python 系列已经写差不多了——Python 的几个主要的特点都介绍了。

      关于 boost
      这个库已经有好几年历史了,也比较成熟了。
      俺觉得 C++ 程序员应该了解一下它。
      俺自己写 C++ 程序,也比较喜欢用它。

      最近几年,因为读者群扩大,很多读者都不是程序员。
      所以编程类的博文,比以前少了,还望谅解。

      删除
  22. primes=[]
    i=3
    while i < 100000:
    if i % 2 != 0:
    flag = 0
    for prime in primes:
    if i % prime == 0:
    flag = 1
    break
    if flag != 1:
    primes.append(i)
    #print i
    i = i + 1

    大佬们 怎么把上述求质数代码缩减到一行里?

    回复删除