【Python系列专栏】第十五篇 Python中的函数式编程(上)

函数式编程

函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。

函数式编程(请注意多了一个**“式”字)——Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算**。

我们首先要搞明白**计算机(Computer)计算(Compute)**的概念。

  • 在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以汇编语言是最贴近计算机的语言。

  • 计算则是指数学意义上的计算,越是抽象的计算,离计算机硬件越远。

对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。

归纳一下

低级语言高级语言
特点贴近计算机贴近计算(数学意义上)
抽象程度
执行效率
例子汇编和CLisp

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数

Python仅对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言

目录

函数式编程的三大特性

  1. immutable data

    变量不可变,或者说没有变量,只有常量。 函数式编程输入确定时输出就是确定的,函数内部的变量和函数外部的没有关系,不会受到外部操作的影响。

  2. first class functions

    第一类函数(也称高阶函数),意思是函数可以向变量一样用,可以像变量一样创建、修改、传递和返回。 这就允许我们把大段代码拆成函数一层层地调用,这种面向过程的写法相比循环更加直观。

  3. 尾递归优化

    之前一章的递归函数中已经提及过了,就是递归时返回函数本身而非表达式。 可惜Python中没有这个特性。



函数式编程的几个技术

  1. map & reduce

    函数式编程最常见的技术就是对一个集合做Map和Reduce操作。这比起传统的面向过程的写法来说,在代码上要更容易阅读(不需要使用一堆for、while循环来倒腾数据,而是使用更抽象的Map函数和Reduce函数)。

  2. pipeline

    这个技术的意思是把函数实例成一个一个的action,然后把一组action放到一个数组或是列表中组成一个action list,然后把数据传给这个action list,数据就像通过一个pipeline一样顺序地被各个函数所操作,最终得到我们想要的结果。

  3. recursing

    递归最大的好处就简化代码,它可以把一个复杂的问题用很简单的代码描述出来。注意:递归的精髓是描述问题,而这正是函数式编程的精髓。

  4. currying

    把一个函数的多个参数分解成多个函数, 然后把函数多层封装起来,每层函数都返回一个函数去接收下一个参数这样,可以简化函数的多个参数(减少函数的参数数目)。

  5. higher order function

    高阶函数:所谓高阶函数就是函数当参数,把传入的函数做一个封装,然后返回这个封装函数。现象上就是函数传进传出。

对currying进行一点补充,举个例子:

def pow(i, j):
    return i**j

def square(i):
    return pow(i, 2)

这里就是把原本平方函数square的参数j分解了,它返回幂函数pow函数,把幂次封装在里面,从而减少了求平方时所需用到的参数。

关于函数式编程的一些概念理解可以看傻瓜函数式编程或者英文原版的Functional Programming For The Rest of Us



函数式编程的几个好处

  1. parallelization 并行

    在并行环境下,各个线程之间不需要同步或互斥(变量都是内部的,不需要共享)。

  2. lazy evaluation 惰性求值

    表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值。

  3. determinism 确定性

    输入是确定的,输出就是确定的。

简单举例

以往面向过程式的编程需要引入额外的逻辑变量以及使用循环:

upname =['HAO', 'CHEN', 'COOLSHELL']
lowname =[]
for i in range(len(upname)):
    lowname.append( upname[i].lower() )

而函数式编程则非常简洁易懂:

def toUpper(item):
  return item.upper()

upper_name = map(toUpper, ["hao", "chen", "coolshell"])
print upper_name

再看一个计算一个列表中所有正数的平均数的例子:

num =[2, -5, 9, 7, -2, 5, 3, 1, 0, -3, 8]
positive_num_cnt = 0
positive_num_sum = 0
for i in range(len(num)):
    if num[i] > 0:
        positive_num_cnt += 1
        positive_num_sum += num[i]

if positive_num_cnt > 0:
    average = positive_num_sum / positive_num_cnt

print average

如果采用函数式编程:

positive_num = filter(lambda x: x>0, num)
average = reduce(lambda x,y: x+y, positive_num) / len( positive_num )

可以看到函数式编程减少了变量的使用,也就减少了出Bug的可能,维护更加方便。可读性更高,代码更简洁

更多的例子和解析详见函数式编程



高阶函数

前面已经提到了函数式编程中的高阶函数特性,这一节将针对Python中的使用方式进行更详细的描述。

变量可以指向函数

>>> abs
<built-in function abs>
>>> f = abs
>>> f
<built-in function abs>
>>> f(-10)
10

这个例子表明在Python中变量是可以指向函数的,并且这样赋值的变量能够作为函数的别名使用。


函数名也是变量

>>> abs = 10
>>> abs(-10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable

这里把abs函数赋值为10,这样赋值以后abs就变成一个整形变量,指向int型对象10而不指向原本的函数了。所以无法再作为函数使用。

想恢复abs函数要重启Python交互环境。 abs函数定义在 __builtin__ 模块中,要让修改abs变量的指向在其它模块也生效,用 __builtin__.abs = 10 就可以了。 当然实际写代码绝对不应该这样做


传入函数

函数能够作为参数传递,接收这样的参数的函数就称为高阶函数。 简单举例:

def add(x, y, f):
    return f(x) + f(y)

>>> add(-5, 6, abs)
11

这里abs函数可以作为一个参数传入我们编写的add函数中,add函数就是一个高阶函数。


map/reduce

map()函数和reduce()函数是Python的两个内建函数(BIF)。

map函数

map()函数接收两个参数,一个是函数,一个是Iterable对象,map将传入的函数依次作用到序列的每个元素,并把结果作为Iterator对象(惰性序列,可以用list转换为列表输出)返回。例如:

>>> def f(x):
...     return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]

这里直接使用list()函数将迭代器对象转换为一个列表。

写循环也能达到同样效果,但是显然没有map()函数直观。 map()函数作为高阶函数,大大简化了代码,更易理解。

>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']

将一个整数列表转换为字符列表仅仅需要一行代码。

reduce函数

reduce接收两个参数,一个是函数(假设该函数称为f),一个是Iterable对象(假设是l)。函数f必须接收两个参数,reduce函数每次会把上一次函数f返回的值和l的下一个元素传入到f中,直到l中所有元素都参与过运算时返回函数f最后返回的值(第一次传入时传入l的头两个元素)。

>>> from functools import reduce
>>> def add(x, y):
...     return x + y
...
>>> reduce(add, [1, 3, 5, 7, 9])
25

这里举了一个最简单的序列求和作例子(当然实际上我们直接用sum()函数更方便,这里只是为了举例子)。 这里reduce函数每次将add作用于序列的前两个元素,并把结果返回序列的头部,直到序列只剩下1个元素就返回结果(这样理解可能更直观一些)。

>>> from functools import reduce
>>> def fn(x, y):
...     return x * 10 + y
...
>>> def char2num(s):
...     return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s] #字符对应整数的dict,返回传入字符对应的整数
...
>>> reduce(fn, map(char2num, '13579'))
13579

可以整理一下,作为一个整体的str2int函数:

def str2int(s):
    def fn(x, y):
        return x * 10 + y
    def char2num(s):
        return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s]
    return reduce(fn, map(char2num, s))

使用lambda匿名函数还可以进一步简化:

def char2num(s):
    return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s]

def str2int(s):
    return reduce(lambda x, y: x * 10 + y, map(char2num, s))
练习

1.利用map()函数,把不规范的英文名字变为首字母大写其他字母小写的规范名字。

Hint:

  • 字符串支持切片操作,并且可以用加号做字符串拼接。
  • 转换大写用upper函数,转换小写用lower函数。
def normalize(name):
    return name[0].upper()+name[1:].lower()

L1 = ['adam', 'LISA', 'barT']
L2 = list(map(normalize, L1))
print(L2)

2.编写一个prod()函数,可以接受一个list并利用reduce()求积。

Hint:

  • 用匿名函数做两数相乘
  • 用reduce函数做归约,得到列表元素连乘之积。
from functools import reduce
def prod(L):
    return reduce(lambda x,y: x*y,L)

print('3 * 5 * 7 * 9 =', prod([3, 5, 7, 9]))

3.利用map和reduce编写一个str2float函数,把字符串’123.456’转换成浮点数123.45。

Hint:

  • 这题的思路是找到小数点的位置i(从个位开始数i个数字之后),然后让转换出的整数除以10的i次方。
  • 另外一种思路是在转换时遇到小数点后,改变转换的方式由 num*10+当前数字 变为 num+当前数字/point。 point初始为1,每次加入新数字前除以10。
from functools import reduce
from math import pow

def chr2num(s):
    return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s]

def str2float(s):
    return reduce(lambda x,y:x*10+y,map(chr2num,s.replace('.',''))) / pow(10,len(s)-s.find('.')-1)

print(str2float('985.64785'))

filter

filter()函数同样是内建函数,用于过滤序列。 filter()接收一个函数和一个Iterable对象。 和map()不同的时,filter()把传入的函数依次作用于每个元素,然后根据函数返回值是True还是False决定保留还是丢弃该元素

简单的奇数筛选例子:

def is_odd(n):
    return n % 2 == 1

list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
# 结果: [1, 5, 9, 15]

筛掉列表的空字符串:

def not_empty(s):
    return s and s.strip()

list(filter(not_empty, ['A', '', 'B', None, 'C', '  ']))
# 结果: ['A', 'B', 'C']

其中,strip 函数用于删除字符串中特定字符,格式为:s.strip(rm),删除s字符串中开头、结尾处的包含在rm删除序列中的字符。 rm为空时默认删除空白符(包括’\n’, ‘\r’, ‘\t’, ’ ')。

注意到 filter() 函数返回的是一个 Iterator对象,也就是一个惰性序列,所以要强迫 filter() 完成计算结果,需要用 list() 函数获得所有结果并返回list。

filter函数最重要的一点就是正确地定义一个筛选函数(即传入filter作为参数的那个函数)。

练习

1.用filter筛选素数

这里使用埃氏筛法

首先,列出从2开始的所有自然数,构造一个序列:

2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...

取序列的第一个数2,它一定是素数,然后用2把序列的2的倍数筛掉:

3, 5, 7, 9, 11, 13, 15, 17, 19, ...

取新序列的第一个数3,它一定是素数,然后用3把序列的3的倍数筛掉:

5, 7, 11, 13, 17, 19, ...

以此类推…

首先构造一个生成器,输出3开始的奇数序列:

def _odd_iter():
    n = 1
    while True:
        n = n + 2
        yield n

然后定义一个筛选函数,传入n,判断x能否除尽n:

def _not_divisible(n):
    return lambda x: x % n > 0

这里x是匿名函数的参数,由外部提供

然后就是定义返回素数的生成器了。

  • 首先输出素数2,然后初始化奇数队列,每次输出队首(必然是素数,因为前一轮的过滤已经排除了比当前队首小且非素数的数)。

  • 构造新的队列,每次用当前序列最小的数作为除数,检验后面的数是否素数。

定义如下:

def primes():
    yield 2
    it = _odd_iter() # 初始序列
    while True:
        n = next(it) # 返回序列的第一个数
        yield n
        it = filter(_not_divisible(n), it) # 构造新序列

这里因为it是一个迭代器,每次使用next就得到队列的下一个元素,实际上就类似队列的出列操作,挤掉队首,不用担心重复。

然后这里filter的原理,就是把当前it队列的每个数都放进_not_divisible(n)中检测一下,注意不是作为参数n传入而是作为匿名函数的参数x传入

_not_divisible(n) 实际是作为一个整体来看的,它返回一个自带参数n的函数(也即那个匿名函数),然后filter再把列表每一个元素一一传返回的匿名函数中。一定要搞清楚这一点。

  • 最后,因为primes产生的也是一个无限长的惰性序列,我们一般不需要求那么多,简单写个循环用作退出即可:
# 打印1000以内的素数:
for n in primes():
    if n < 1000:
        print(n)
    else:
        break

2.用filter筛选回文数

Hint:

  • str可以把整数转换为字符串
  • [::-1]可以得到逆序的列表。
def is_palindrome(n):
    return str(n) == str(n)[::-1]

print(list(filter(is_palindrome, range(0,1001))))

sorted

Python内置的 sorted() 函数就可以对list进行排序:

>>> sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]

并且 sorted() 作为高阶函数还允许接受一个key函数用于自定义排序,例如:

>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]

key指定的函数将作用于list的每一个元素上,并根据key函数返回(映射)的结果进行排序,最后对应回列表中的元素进行输出。

再看一个字符串排序例子:

>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']

默认情况下是按ASCII码排序的,但我们往往希望按照字典序来排,思路就是把字符串变为全小写/全大写再排:

>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
['about', 'bob', 'Credit', 'Zoo']

默认排序是由小到大,要反相排序只需把reverse参数设为True。 温习前面的知识,这里reverse参数是一个命名关键字参数

>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
['Zoo', 'Credit', 'bob', 'about']

使用好sorted函数的关键就是定义好一个映射函数。

练习

给出成绩表,分别按姓名和成绩进行排序。

>>> L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
>>> L2 = sorted(L, key = lambda x:x[0])    #按姓名排序
>>> L2
[('Adam', 92), ('Bart', 66), ('Bob', 75), ('Lisa', 88)]
>>> L3 = sorted(L, key = lambda x:x[1])    #按成绩排序
>>> L3
[('Bart', 66), ('Bob', 75), ('Lisa', 88), ('Adam', 92)]


返回函数

函数作为返回值

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。

比方说我们想实现一个对可变参数求和的函数,可以这样写:

def calc_sum(*args):
    ax = 0
    for n in args:
        ax = ax + n
    return ax

调用时可以传入任意个数字,并得到这些数字的和。而如果我们不需要立即求和,而是后面再根据需要来进行求和,可以写为返回求和函数的形式:

def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum

在调用 lazy_sum 时,返回一个sum函数,但sum函数内部的求和代码没有执行:

>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>

当我们再调用返回的这个sum函数时,就能得到和值了:

>>> f()
25

注意!每一次调用 lazy_sum 返回的函数都是不同的!即使传入相同的参数,返回函数也是不同的!举个例子:

>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False

f1和f2是不同的两个函数,虽然调用它们得到同样的结果,但它们是互不影响的。


闭包

在Python中,从表现形式上来讲,闭包可以定义为:如果在一个内部函数里,对外部作用域(非全局作用域)的变量进行了引用,那么这个内部函数就被认为是闭包(closure)。 如上面例子中的f就是一个闭包,它调用了变量i,变量i属于外面的循环体而不是全局变量。

看一个例子:

def count():

    fs = []
    for i in range(1, 4):
        def f():
             return i*i
        fs.append(f)

    return fs

f1, f2, f3 = count()

三个返回函数的调用结果是:

>>> f1()
9
>>> f2()
9
>>> f3()
9

解析一下,这里count函数是一个返回三个函数的函数,里面使用了一个循环体来产生要返回的三个函数。从i为1开始到i等于3,每次产生一个函数f,返回i的平方值。如果按照平常的思路来看,可能会觉得返回的三个函数f1、f2、f3应该分别输出1、4、9。

但实际上并不是这样的,这是因为返回一个函数时,函数内部的代码是没有执行的! 只有在调用这个返回的函数时才会执行

调用count函数时,实际上返回了3个新的函数,循环变量i的值也变为3。在调用这3个返回的函数时,它们的代码才会执行,这时引用的i的值就都是3。

如果一定要在闭包中用到外部的循环变量,要怎么办呢? 我们先定义一个函数,用它的参数绑定循环变量,然后再在它的里面定义要返回的函数。 这样无论后面循环变量怎么变,已经绑定到参数的值是不会变的,就能得到我们期望的结果了。也即把上面的例子改写为:

def count():

    def f(j):
        def g():
            return j*j
        return g

    fs = []
    for i in range(1, 4):
        fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
    return fs

调用结果:

>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9

这里闭包g用到的变量j是外部作用域f的,并且j作为参数绑定在f中不再改变,不同于外部作用域count函数中的变量i。 所以执行count返回的3个函数,每个的结果都不同。

总结
  • 返回闭包时,不要在闭包的代码中引用外部作用域的循环变量或者外部作用域中会变化的变量

  • 不应该在闭包中修改外部作用域的局部变量



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mrrunsen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值