11.1. 什么是函数式编程

常见的编程范式有命令式编程、函数式编程、逻辑式编程。

函数式编程是一种编程范式,看待问题的一种方式,每一个函数都是为了用小函数组织成更大的函数,函数的参数也是函数,函数返回的也是函数。常见的面向对象编程是也是一种命令式编程。

函数式编程可以被认为是面向对象编程的对立面。

一些语言的设计者选择强调一种特定的编程方式,这通常会让以不同的方式来编写程序变得困难。 其他多范式语言则支持几种不同的编程方式。

Lisp,C++ 和 Python 都是多范式语言;使用这些语言,你可以编写主要为过程式,面向对象或者函数式的程序和函数库。 在大型程序中,不同的部分可能会采用不同的方式编写;比如 GUI 可能是面向对象的,而处理逻辑则是过程式或者函数式。

编程语言的几种结构方式

  • 过程式 所谓程序就是一连串告诉计算机怎样处理程序输入的指令。C、Pascal 甚至 Unix shells 都是过程式语言。

  • 声明式 SQL 可能是比较熟悉的声明式语言了。 一个 SQL 查询语句描述了你想要检索的数据集,并且 SQL 引擎会决定是扫描整张表还是使用索引,应该先执行哪些子句等等。

  • 面向对象 程序会操作一组对象。 对象拥有内部状态,并能够以某种方式支持请求和修改这个内部状态的方法。Smalltalk 和 Java 都是面向对象的语言。 C++ 和 Python 支持面向对象编程,但并不强制使用面向对象特性。

  • 函数式 函数式编程则将一个问题分解成一系列函数。 理想情况下,函数只接受输入并输出结果,对一个给定的输入也不会有影响输出的内部状态。

在函数式程序里,输入会流经一系列函数。每个函数接受输入并输出结果。函数式风格反对使用带有副作用的函数,这些副作用会修改内部状态,或者引起一些无法体现在函数的返回值中的变化。完全不产生副作用的函数被称作“纯函数”。消除副作用意味着不能使用随程序运行而更新的数据结构;每个函数的输出必须只依赖于输入。

函数式风格的 Python 程序并不会极端到消除所有 I/O 或者赋值的程度;相反,他们会提供像函数式一样的接口,但会在内部使用非函数式的特性。比如,函数的实现仍然会使用局部变量,但不会修改全局变量或者有其他副作用。

11.1.1. 函数式编程的特点

  • 模块化

    函数式编程的一个更实用的优点是,它强制你把问题分解成小的方面。因此程序会更加模块化。 相对于一个进行了复杂变换的大型函数,一个小的函数更明确,更易于编写, 也更易于阅读和检查错误。

  • 易于调试

    调试很简单是因为函数通常都很小而且清晰明确。 当程序无法工作的时候,每个函数都是一个可以检查数据是否正确的接入点。 你可以通过查看中间输入和输出迅速找到出错的函数。

  • 组合性

    小的函数更容易加以组合形成新的功能。

人们通常都认为函数式很深奥,而且与可实践性相比,它更看重优雅和简洁。大公司 很少 会在大规模项目上依赖于函数式为主的语言,即使要用也是在较小的范围内,实际上函数式编程只是一种考虑逻辑流的方式,它本身也有优点和缺点,而且也能与其他编程范式配合使用。

11.1.2. 函数

函数(function)的定义通常分为传统定义和近代定义,函数的两个定义本质是相同的,只是叙述概念的出发点不同。 传统定义是从运动变化的观点出发,而近代定义是从集合、映射的观点出发。

函数式编程关心数据的映射,命令式编程关心解决问题的步骤。这里的映射就是数学上“函数”的概念。

尽管Python并不是以函数式为主的语言,但对它来说支持函数式编程也相对比较容易,因为Python中的一切都是对象。这意味着函数定义也可以赋给变量并传递。

>>> def add(a, b):
>>>     return a + b
>>>
>>> plus = add
>>>
>>> plus(3, 4)
7

11.1.3. 函数式编程的特性

因为函数式风格没有赋值,也就没有for循环, 要实现循环操作只能通过递归调用。 由于变量值是不可变的,对于值的操作并不是修改原来的值,而是修改新产生的值; 同样由于变量不可变,纯函数编程语言无法实现循环。但是绝对纯净的函数也是无法实现的。

lambda

通过 Lambda 表达式的语法,可以用声明式的方式创建函数。它的历史比函数式编程还要久远。这一概念的另一个术语叫做“匿名函数”,因为 lambda 函数可以直接嵌入到行内使用,不需要事先指定名称。将匿名函数赋值给变量后,它的行为与正常函数完全一样。使用起来简单易读:

>>> (lambda a, b: a + b)(3, 4)
7

上面的语句与下面是等价的。下面的语句先给这个“匿名函数”进行了命名:

>>> addition = lambda a, b: a + b
>>> addition(3, 4)
7

内置模板函数

函数式风格尽量避免使用变量,达到‘纯’函数, 仅仅为了控制流程而定义的循环变量和流程中产生的临时变量无疑是最需要避免的。

下面介绍几个内置函数:

  • reduce

Python有一个内建函数reduce,函数将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。

像Python这样构建于类C语言之上的函数式语言,由于语言本身提供了编写循环代码的能力,内置函数虽然提供函数式编程的接口,但一般在内部还是使用循环实现的。 同样的,如果发现内建函数无法满足你的循环需求,不妨也封装它,并提供一个接口,这种方式可以有效地提高效率。

>>> import operator, functools
>>> functools.reduce(operator.concat, ['A', 'B', 'C',''])
'ABC'

如果你在 functools.reduce() 中使用 operator.add(),你就会把可迭代对象中的所有元素加起来.这种情况非常常见, 所以 Python 有一个特殊的内置函数 sum():

>>> functools.reduce(operator.add, [1, 2, 3, 4])
10

不过, 对于很多使用 functools.reduce() 的情形, 使用明显的 for 循环会更清晰,常用的还有以下三个函数

  • map(function, iterable, …)

map可以接受多个iterable作为参数,在第n次调用function时,将使用iterable1[n], iterable2[n], …作为参数。

  • filter(function, iterable)

功能是过滤出iterable中所有以元素自身作为参数调用function时返回True或bool(返回值)为True的元素并以列表返回,与系列第一篇中的my_filter函数相同。

  • zip(iterable1, iterable2, …)

函数返回一个列表,每个元素都是一个元组,包含(iterable1[n], iterable2[n], …)。 如果参数的长度不一致,将在最短的序列结束时结束;如果不提供参数,将返回空列表。

函数作为参数

如果你OOP的方法模式很熟悉,相信你能很快速地学会将函数当作参数传递。让我们用一个示例看一下

>>> def adds(lst):
>>>     count = 0
>>>     for num in lst:
>>>         count = add(amount, num)
>>>     return count

方法中想要计算加法,如果想要计算减法、乘法、或者是除法呢是不是还要再写三个方法一次调用。答案肯定是不用的。

>>> def calculate(function, lst, init):
>>>     result = init
>>>     for num in lst:
>>>         result = function(result, num)
>>>     return result
>>> calculate(add, range(5), 0)
10

函数作为返回值

将函数返回通常需要与闭包一起使用(即返回一个闭包)才能发挥威力。闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。

闭包

闭包包含自由变量,这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。 “闭包” 一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境,闭包是一类特殊的函数。