枕边的函数“氏”

爱江山更爱美人,枕边佳人,你值得拥有。

1 美人心计

今日向大家介绍后花园中的3位美人,分别是命令氏、面向对象氏、函数氏。

  • 命令氏,妃,贤妻良母,夫唱妇随,和她在一起四字可形容,简单粗暴;
  • 面向对象氏,嫔,聪明伶俐,足智多谋,心眼一个接着一个,省了我不少事情;
  • 函数氏,贵人,零零后,八面玲珑,火星语乱飞,女孩的心思你别猜。

她们国色天香,楚楚动人,大概是下面这个样子:

2 求和问题

我曾遇到“求和问题”,美人傍身各施其计。问题如下:

  1. 求x和y的和(测试用例:x=1,y=2)

  2. 求x和y的平方的和(测试用例:x=1,y=2)

  3. 求x的m次方和y的n次方的和(测试用例:x=1,y=2,m=3,n=4)

2.1 命令氏

命令氏的方法简单粗暴,可行且有效,但是有些笨拙。定义3个函数sum1sum2sum3,依次解决上面的3个问题。

def sum1(x, y):
    return x + y

def sum2(x, y):
    return x**2 + y**2

def sum3(x, y, m, n):
    return x**m + y**n

sum1(1, 2) # 3
sum2(1, 2) # 5
sum3(1, 2, 3, 4) # 17

2.2 面向对象氏

面向对象氏的方法要美得多,两个字“优雅”。所以,面向对象氏一直是枕边佳人,恩宠备至。封装、继承、多态是面向对象氏的特色,对于sum函数,传入不同的参数将会触发不同的计算过程。放到C++里描述,sum可以写成2个函数,int Sum::sum(void)int Sum::sum(int, int)

class Sum:

    def __int__(self, x, y):
        self.x, self.y = x, y

    def sum(self, m=1, n=1):
        return self.x**m + self.y**n

s = Sum(1, 2)
s.sum() # 3
s.sum(2, 2) # 5
s.sum(3, 4) # 17

2.3 函数氏

函数氏的方法要奇特的多,乍一看有点笨,但却是完全不一样的思想。

def sum(x, y, f, g):
    return f(x) + g(y)

def f1(x):
    return x

def f2(x):
    return x**2

def f3(x):
    return x**3

def f4(x):
    return x**4

sum(1, 2, f1, f1) # 3
sum(1, 2, f2, f2) # 5
sum(1, 2, f3, f4) # 17

解决了今日的“求和问题”,可能会遇到明天的“求差问题”,问题无穷匮也。而我们心中的困惑在于,如何确定编码的粒度,就像做一款产品,哪些功能该加,哪些功能不该加,哪些功能暂时不加,如果某一个功能以后要加,能不能很好的融入到已有的产品中。

3 翻牌函数氏

函数氏的功劳在于抽象出了sum的求解公式:$sum(x,y)=f(x)+g(y)$,非常有数学风啊哈。但是,方案还是有瑕疵。4个函数$f_1,f_2,f_3,f_4$分别用于计算$x_k,k \in \{1,2,3,4\}$。当我们需要计算$x_{k'},k' \in [1,100]$怎么办?劳心者治人,我们可以做得更漂亮,比如这样。

def sum(x, y, f, g):
    return f(x) + g(y)

def generate_func(k):
    def func(x):
        return x**k
    return func

sum(1, 2, generate_func(1), generate_func(1)) # 3
sum(1, 2, generate_func(2), generate_func(2)) # 5
sum(1, 2, generate_func(3), generate_func(4)) # 17

如果又有一个新的问题,求x的1/m的n次方和y的和(测试用例:x=6,y=6,m=3,n=4),函数氏解决起来会更加酸爽。

def sum(x, y, f, g):
    return f(x) + g(y)

def generate_func2(m, n):
    def func(x):
        return (x/m)**n
    return func

sum(6, 6, generate_func2(3, 4), generate_func(1)) # 22

可以看到,函数氏最大的魅力在于,函数和数据都可以作为输入,优势很明显,除了喂给你数据,还可以控制你如何吃数据,一切尽在掌握。

3.1 高阶函数(Higher-order Function)

generate_func着实帮了大忙,他可以动态生成函数,而无需人工蛮力定义。除了省时省力,最大的好处是生成的函数有无限个,这是人工所不能及的。

高阶函数就是generate_func这样的函数,要么输入中至少有一个函数,要么输出一个函数,至少满足两个条件中的一个。何谓高阶?高低是需要比较的,人比小兔子要高阶,大学数学比初中数学要高阶,我心里的她要比其他人高阶,这就是高阶。高阶函数比普通意义的函数高阶。

为什么高阶函数更牛逼?有一个词叫“泛化”,是从具体到抽象,抽象可以让我们站在更高的位置看待这芸芸众生,然后悟出什么人生道理。什么模块化、面向对象、设计模式blabla,我们不都是在追求抽象,追求“泛”吗?从一个函数生成无数个函数,这不就是四两拨千斤,一生二二生三嘛!

所以高阶函数实至名归。既然高阶函数这么“泛”,为啥不叫“泛函数”?

高阶函数在数学中也叫做算子(运算符)或泛函。

3.2 闭包(Closure)

当我们使用高阶函数来生成函数的时候,可以使用以下两种方法。方法一是将要生成的函数f写在高阶函数gen_f内部;方法二是将f写在外部。通常我们使用第一种,这就和定义局部变量差不多,在哪用就在哪定义,肥水不流外人田,减少对外界的污染。

# method 1
def gen_f():
    def f():
        pass
    return f

# method 2
def f():
    pass
def gen_f():
    return f

一旦我们承认并习惯使用第一种方法的时候,就可能会写出以下风格的代码。

def gen_f():
    array = []
    def f():
        array.append(1)
        return array
       return f

func = gen_f()
func() # [1]
func() # [1, 1]
func() # [1, 1, 1]

预料之外,情理之中,f就是闭包函数。func作为生成出来的函数,每次调用时都会往array里放一个数字1,而array是在外部的gen_f中定义的。这时就需要作出选择,是修改外部的array还是抛出一个找不到array的错误。支持闭包特性的编程语言选择的是前者。

闭包函数和其引用的变量将一同存在,所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。

3.3 柯里化(Currying)

如果我们习惯了生成函数带来的快感,也很有可能写出以下代码。方法一和方法二都实现了同样的功能,不同之处在于方法一每次只传一个参数,而方法二一次性把所有参数传入进去。

# method 1
def f(x):
    def g(y):
        def h(z):
            return x + y + z
        return h
    return g
f(1)(2)(3) # 6

# method 2
def f(x, y, z):
    return x + y + z
f(1, 2, 3) # 6

尽管方法一看起来有点啰嗦,但这也意味函数氏在我们心中的地位大大提升。有点“后宫佳丽三千,却偏偏宠她一人”的感觉啊哈。

从方法二到方法一的变换称为柯里化,即把接受多个参数的函数变成每次只接受一个参数的函数,并返回接受余下的参数的函数。有点拗口,就是通过多次调用函数来代替一次传入多个参数。

柯里化的优势在于可以将抽象的函数具体化,比如打印日志。

def print_msg(label, msg):
    print '[%s] %s' % (label, msg)

# no currying    
print_msg('error', 'network failed')
print_msg('info', 'init ok')

# use currying
print_err_msg = curry(print_msg)('error')
print_info_msg = curry(print_msg)('info')

print_err_msg('network failed')
print_info_msg('init ok')

其中,函数curry表示将输入的函数柯里化。

3.4 偏函数(Partial Function)

坏消息是Python并没有提供curry函数,好消息是有一些第三方的可以实现该效果,比如https://github.com/kachayev/fn.py,这个库可以给Python插上函数式编程的翅膀。

Python的内置functools模块提供了类似curry的功能,名曰偏函数。

from functools import partial

def print_msg(label, msg):
    print '[%s] %s' % (label, msg)

# use partial
print_err_msg = partial(print_msg, 'error')
print_info_msg = partial(print_msg, 'info')

print_err_msg('network failed')
print_info_msg('init ok')

柯里化和偏函数类似但不同,柯里化是将多参数函数转变为一系列单参数函数的链式调用,而偏函数是事先固定好一部分参数后面就无需重复传入了。两者都可以实现函数的具体化,固定函数的一部分参数来达到特定的应用。

3.5 匿名函数

使用函数原来可以如此之爽,恩,函数有意思。但是函数的定义着实是个麻烦,我们曾经写过以下代码。

def sum(x, y, f, g):
    return f(x) + g(y)

def f1(x):
    return x

def f2(x):
    return x**2

def f3(x):
    return x**3

def f4(x):
    return x**4

sum(1, 2, f1, f1) # 3
sum(1, 2, f2, f2) # 5
sum(1, 2, f3, f4) # 17

烦。此时,匿名函数的威力便可发挥出来了,如下。lambda可以使我们达到快速定义并使用函数的效果,绿色无污染,干净利索,棒!

def sum(x, y, f, g):
    return f(x) + g(y)

sum(1, 2, lambda x:x, lambda x:x) # 3
sum(1, 2, lambda x:x**2, lambda x:x**2) # 5
sum(1, 2, lambda x:x**3, lambda x:x**4) # 17

3.6 map、reduce、filter

map、reduce、filter是Python内置的高阶函数,通过传入函数可以实现某些特定的功能,通过使用这些函数可以让代码更加简洁,逼格更上一层楼。

array = [1,2,3]

### [1,2,3]变换为[1*1,2*2,3*3]
# bad
result = []
for i in array:
    result.append(i*i)

# good
map(lambda x:x*x, array) # [1,4,9]


### 求[1,2,3]中元素的和
# bad
result = 0
for i in array:
    result += i

# good
reduce(lambda x,y:x+y, array) # 6


### 求[1,2,3]中的奇数
# bad
result = []
for i in array:
    if i % 2:
        result.append(i)  

# good
filter(lambda x:x%2, array) # [1,3]

参考