函数是一等公民

虽然《流畅的Python》作者一再强调 Python 不是一门函数式编程语言,但它的的确确具备了一些函数式编程的特性。其中的一个重要特性是:Python 将函数作为一等公民。这与 JavaScript、Scala 等语言一样,意味着在这类语言中:函数与其他数据类型处于同等地位,函数可以定义在函数内部,也可以作为函数的参数和返回值。基于这个特性,我们可以很容易的定义高阶函数。来看一个 JavaScript 的例子:

const add = function(x) {
  return function(y) {
    return x + y
  }
}

这个函数将一个函数作为了返回值,很明显它是一个高阶函数,那么问题来了:这样定义有什么作用或者是好处呢?事实上,这段代码是 JavaScript 中的一个优雅的函数式编程库 Ramdaarrow-up-right 对于加法实现的基本思路(还需要可变参数以及参数个数判断)。最终我们可以这样去使用它:

const R = require('ramda')
R.add(1, 2) // -> 3
const increment = R.add(1) // 返回一个函数
increment(2) // -> 3
R.add(1)(2) // -> 3

既可以像代码第二行一次性传入两个参数,也可以像代码第三、四行分两个阶段传入,这与代码第五行效果一致。我们将这种特性称为函数柯里化(Currying),这样做的好处一是可以代码重用,就像特意将 R.add(1) 取名为 increment 一样,它可以单独地作为一个递增函数;二是可以实现惰性求值,只有当函数收集到了所有所需参数,才进行真正的计算并返回结果,这一点在许多流处理框架中有广泛使用。

Python 中的函数之所以可以作为一等公民,究其原因,是因为 Python 中的一切皆是对象,即 Everything in Python is an object。使用 def 关键字定义的任何函数,都是 function 类的一个实例。

>>> def func():
...     pass
... 
>>> type(func)
<class 'function'>

既然函数是对象,那就可以持有属性,这也是为什么 Python 中函数可以持有外部定义的变量(也就是闭包问题)的根本原因。这一点与 Java 和 C++ 这类语言是有本质区别的。以 Java8 为例,虽然 Java8 提供了一些语法糖让我们得以编写所谓的“高阶函数”,但 Java 中的函数(方法)依然不能脱离类或者对象而存在:

上述代码第三行接收一个 Lambda 表达式作为参数,第四行接收一个方法引用,看上去函数可以作为参数传入。但实际上,Java 编译器会将它们转换为函数接口(Functional Interfaces)的具体实现,函数接口是 Java8 函数式编程引入的核心概念。例如上述代码中的 System.out::println 方法引用会被实例化为 Consumer 函数接口的具体实现,Consumer 是 Java8 提供的四类函数接口中的一类,称为消费者接口,它有一个 accept 抽象方法接受一个输入且返回值为空,编译器将会用 System.out.println(t) 重写这个方法。

所以在 Java8 中,看似函数可以作为参数传入,但实际上传入的依旧是类的实例。如果对 Java8 的函数式编程感兴趣可以参考这篇:Java8 函数接口arrow-up-right

言归正传,既然已经清楚了 Python 中可以定义高阶函数,那么接下来就可以探讨一下 Python 怎么使用高阶函数实现装饰器的。但在这之前,不得不提及一下什么是闭包。

Last updated

Was this helpful?