跳转至

函数式编程 Functional Programming

在Python和很多编程语言中,函数式编程Functional Programming一定是当之无愧的降龙十八掌最后一掌,威力无穷。

如果需要专业指导,官方中文文档对函数式编程有特别专业的指引:函数式编程指引,本文就浅浅得带你入个门。


什么是函数式编程

函数式编程,它是一种编程方式,是一种思维方式,是对编程的认知方式。它与面对对象的编程方式是同一个层次对编程的思考。

我说了那么多,原因是,我暂时也整理不出合适的白话来讲什么是函数式编程。作为一种编程思想,它必然需要些专业术语和概念来做铺垫。

所以像这种问题,我一般推荐直接阅读百科词条:函数式编程


高阶函数

接受一个函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。

例如:

def add(a, b, f):
    return f(a) + f(b)

add函数接收3个参数,分别是待处理的a,bf函数,add函数本身并不对数据ab做任何处理,只处理最后f函数计算出来的结果。 这样的过程,可类比于高中数学中的复合函数的概念,即:

add = g(f(x))

编写高阶函数,就是让函数的参数能够接收别的函数。

Python 内建的高阶函数map() reduce() filter sorted()


map()

map()函数接收两个参数,一个是函数,一个是Iterable(例如:list, tuple, set, dict),

map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。

首先,我们需要一个函数用于数据处理:

def f(x):
    return x*x

其次,将函数和待处理的数据传入map函数:

# 接收list
r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
print(list(r))
# 接收tuple
r = map(f, (1, 2, 3, 4, 5, 6, 7, 8, 9))
print(list(r))

输出:

[1, 4, 9, 16, 25, 36, 49, 64, 81]
[1, 4, 9, 16, 25, 36, 49, 64, 81]

map()传入的第一个参数是f,即函数对象本身。由于结果f是一个IteratorIterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list

惰性求值(英语:Lazy Evaluation),又译为惰性计算、懒惰求值,也称为传需求调用(call-by-need),是一个计算机编程中的一个概念,它的目的是要最小化计算机要做的工作。它有两个相关而又有区别的含意,可以表示为“延迟求值”和“最小化求值”.除可以得到性能的提升外,惰性计算的最重要的好处是它可以构造一个无限的数据类型。

Python的惰性序列多数指Iterator,其特点正如同上文所述,具有惰性计算特点的序列称为惰性序列。

这样做的好处在于,f函数对数据的预处理过程是一个可变的过程,map函数无需关心f函数的实现,例如:

def f(x):
    return 2*x
r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
print(list(r))

输出:

[2, 4, 6, 8, 10, 12, 14, 16, 18]

reduce()

reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算: 比方说把序列[1, 3, 5, 7, 9]变换成整数13579:

from functools import reduce
def fn(x, y):
    return x * 10 + y
r = reduce(fn, [1, 3, 5, 7, 9])
print(r)

filter()

filter()函数用于过滤序列。例如,过滤掉[1,2,3,4,5,6,7,8,9,10]中的偶数,只返回奇数:

def is_odd(n):
    return n % 2 == 1
list(filter(is_odd, [1,2,3,4,5,6,7,8,9,10]))
# 结果: [1, 3,5, 7, 9]

sorted()

sorted()接收一个key函数来实现自定义的排序,例如[36, 5, -12, 9, -21]按绝对值大小升序排列:

sorted([36, 5, -12, 9, -21], key=abs)
#结果:[5, 9, -12, -21, 36]

进行反向排序,不必改动key函数,可以传入第三个参数reverse=True,例如[36, 5, -12, 9, -21]按绝对值大小降序排列:

sorted([36, 5, -12, 9, -21], key=abs, reverse=True)
#结果:[36, -21, -12, 9, 5]

闭包

闭包(英语:Closure),在一个外函数中定义了一个内函数,内函数里运用了外函数的临时变量,并且外函数的返回值是内函数的引用。这样就构成了一个闭包。

def outer_func():
    loc_list = []

    def inner_func(name):
        loc_list.append(len(loc_list) + 1)
        print('%s loc_list = %s' % (name, loc_list))
    return inner_func


clo_func_0 = outer_func()
clo_func_0('clo_func_0')
clo_func_1 = outer_func()
clo_func_1('clo_func_1')
clo_func_0('clo_func_0')
clo_func_1('clo_func_1')

输出结果:

clo_func_0 loc_list = [1]
clo_func_1 loc_list = [1]
clo_func_0 loc_list = [1, 2]
clo_func_1 loc_list = [1, 2]

在这个例子中我们至少可以对闭包中引用的自由变量有如下的认识:

  • 闭包中的引用的自由变量只和具体的闭包有关联,闭包的每个实例引用的自由变量互不干扰。
  • 一个闭包实例对其自由变量的修改会被传递到下一次该闭包实例的调用。

在python中一切都是对象,函数也是对象,clo_func_0对象里的loc_listclo_func_1对象里的loc_list自然指向了不同的内存地址。

又例,使用闭包计算一个数的 n 次幂:

#闭包函数,其中 exponent 称为自由变量
def nth_power(exponent):
    def exponent_of(base):
        return base ** exponent
    return exponent_of # 返回值是 exponent_of 函数

square = nth_power(2) # 计算一个数的平方
cube = nth_power(3) # 计算一个数的立方

print(square(2))  # 计算 2 的平方
print(cube(2)) # 计算 2 的立方

输出:

4
8

外部函数 nth_power() 的返回值是函数 exponent_of(),而不是一个具体的数值。

需要注意的是,在执行完 square = nth_power(2)cube = nth_power(3) 后,外部函数 nth_power() 的参数 exponent 会和内部函数 exponent_of 一起赋值给 squrecube,这样在之后调用 square(2) 或者 cube(2) 时,程序就能顺利地输出结果,而不会报错说参数 exponent 没有定义。


匿名函数 lambda

在Python中,对匿名函数提供了有限支持。还是以map()函数为例,计算f(x)=x^2时,除了定义一个f(x)的函数外,还可以直接传入匿名函数:

list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
# 结果为[1, 4, 9, 16, 25, 36, 49, 64, 81]

通过对比可以看出,匿名函数lambda x: x * x实际上就是:

def f(x):
    return x * x

关键字lambda表示匿名函数,冒号前面的x表示函数参数。

匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。


装饰器

我们使用闭包来实现一个签到程序:

import time

def decorator(func):
    def now():
        print(time.strftime('%Y-%m-%d', time.localtime(time.time())))
        func()

    return now

def sign_in():
    print('Pie 签到')

f = decorator(sign_in)
f()

输出:

2020-07-16
Pie 签到

上面的代码演示了装饰器,它们封装一个函数,并且用这样或者那样的方式来修改它的行为。装饰器最典型的应用就是在授权(Authorization)日志(Logging),如上例,我们把它改写成:

#9点之后签到显示'迟到,扣钱'
#此段代码只做简单演示,只判断当前小时是否大于9点
import time

def decorator(func):
    struct_time = time.localtime()
    now_hour = struct_time.tm_hour

    def now():
        print(time.strftime('%Y-%m-%d %H', time.localtime(time.time())))
        func()

    def late():
        print(time.strftime('%Y-%m-%d %H', time.localtime(time.time())))
        print('迟到,扣钱')
        func()

    if now_hour >= 9:
        return late
    else:
         return now

def sign_in():
    print('Pie 签到')


f = decorator(sign_in)
f()

装饰器的 “语法糖”

Python 设计出了 @ 语法糖,使得定义和使用装饰器更方便,这样的语法糖是Python装饰器一大特色。使用语法糖改写上例:

import time

def decorator(func):
    struct_time = time.localtime()
    now_hour = struct_time.tm_hour

    def now():
        print(time.strftime('%Y-%m-%d %H', time.localtime(time.time())))
        func()

    def late():
        print(time.strftime('%Y-%m-%d %H', time.localtime(time.time())))
        print('迟到,扣钱')
        func()

    if now_hour >= 9:
        return late
    else:
         return now

@decorator
def sign_in():
    print('Pie 签到')

sign_in()

通过对比发现,装饰器极大的便利了函数调用和装饰的过程,并且,当你需要更改装饰器时,只需要重新写一个装饰器,再修改为新的装饰器即可,例:

import time
#此段代码无实际意义,只做装饰器代码灵活性的说明
def decorator(func):
    struct_time = time.localtime()
    now_hour = struct_time.tm_hour

    def now():
        print(time.strftime('%Y-%m-%d %H', time.localtime(time.time())))
        func()

    def late():
        print(time.strftime('%Y-%m-%d %H', time.localtime(time.time())))
        print('迟到,扣钱')
        func()

    if now_hour >= 9:
        return late
    else:
         return now

def decorator2(func):
    def weekend():
        print('周末不上班')
        func()

    return weekend

#@decorator
@decorator2
def sign_in():
    print('Pie 签到')

sign_in()

装饰器参数

装饰器中的函数可以使用 *args 可变参数,可是仅仅使用 *args 是不能完全包括所有参数的情况,比如关键字参数就不能了,为了能兼容关键字参数,我们还需要加上 **kwargs

因此,结合参数修改上例:

#通过输入可变参数,实现请假和出差显示考勤正常
import time

def decorator(func):
    struct_time = time.localtime()
    now_hour = struct_time.tm_hour

    def now(*args, **kwargs):
        print(time.strftime('%Y-%m-%d %H', time.localtime(time.time())))
        func(*args, **kwargs)

    def late(*args, **kwargs):
        print(time.strftime('%Y-%m-%d %H', time.localtime(time.time())))
        if kwargs and kwargs['reason']:
            print('考勤正常,原因:%s' % kwargs['reason'])
        else:
            print('迟到,扣钱')
        func(*args, **kwargs)

    if now_hour >= 9:
        return late
    else:
        return now


@decorator
def sign_in(name, **kwargs):
    print('%s 签到' % name)


sign_in('小头')
print('- '*10)
sign_in('大头', apartment='技术部', job='Python工程师', reason='事假')
print('- '*10)
sign_in('Pie', apartment='技术部', job='CTO', reason='出差')

输出:

2020-07-16 10
迟到,扣钱
小头 签到
- - - - - - - - - - 
2020-07-16 10
考勤正常,原因:事假
大头 签到
- - - - - - - - - - 
2020-07-16 10
考勤正常,原因:出差
Pie 签到

参考链接: