函数式编程

高阶函数

map()和reduce()

map()函数接收两个参数,一个是函数,一个是Iterable对象,map()函数将传入的参数函数依次作用在Iterable对象的每一个元素上,并将结果作为新的Iterable对象返回。

注意:
map()函数返回的Iterable对象实际上是一个Iterator不能直接print()输出,可以通过list()函数将其转换然后输出,当然也可以使用循环输出。

1
2
3
4
5
def squre(x):
return x * x

result = map(squre, [1, 2, 3, 4])
print(list(result))

输出结果:

1
2
>>>
[1, 4, 9, 16]

相比于map()函数将参数函数作用到序列的每一个元素而言,reduce()函数则是除了和map()函数一样同时将结果和序列的下一元素进行累加计算。

1
reduce(func, [x1, x2, x3, x4, x5]) = func(func(func(func(x1, x2), x3), x4), x5)

可能大家可能有点疑问就是,拿一个的reduce()函数就为了做一个累加求和运算是不是有点大材小用了,求和不是有sum()函数么?当然求和可以直接使用sum()函数,没必要使用reduce()函数,但是如果要执行将[1, 2, 3, 5, 9]变换成整数12359reduce()函数就很容解决这个问题。

1
2
3
4
5
from functools import reduce
def fn(x, y):
return x * 10 + y

reduce(fn, [1, 2, 3, 5, 9])

想要了解MapReduce大家可以查看论文《MapReduce:Simplified Data Processing on Large Clusters》
要是无法访问Google网站,大家可以在主页下面的Article文件夹下面查找这边我已经下载好的这篇文章

filter()

Python内建filter()函数主要用于过滤序列,和MapReduce一样也接收一个参数函数和一个序列,filter()将传入的参数函数依次作用于序列的每一个元素,然后根据返回值(bool值)来决定是否丢弃该元素。如何使用filter()函数取决于参数函数,也就是在于正确实现一个“筛选函数”。比如要实现一个筛选素数的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def _odd_iter():
n = 1
while True:
n = n + 2
yield n

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

def prines():
yield 2
it = _odd_iter()
while True:
n = next(it)
yield n
it = filter(_not_divisiable(n), it)

# 打印1000以内的素数
for n in prines():
if n < 1000:
print(n)
else:
break

sorted()

sorted()函数除了基础的排序功能外,sorted()函数也是一个高阶函数,它可以接受一个key函数来实现自定义的排序,比如按绝对值大小进行排序:

1
sorted([30, -12, 5, 9, -21], key=abs)

sorted()函数对字符串进行排序默认的是按照ASCII的大小来进行比较,比如Z < a。要实现按照字母的顺序进行排序,只需要使用key函数将字符串全部变成大写(key=str.upper)或者小写(str.lower),然后再比较即可。要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True

返回函数

顾名思义就是把一个函数作为返回值返回,先了解一下函数的参数,Python的函数定义除了正常的必选参数外,还可以使用默认参数、可变参数和关键字参数,使得函数的定义出来的接口不仅能处理复杂的参数还可以简化代码。

1.位置参数

比如要实现一个可以计算$x^n$的函数pwd(x, n),对于函数来说,参数x, n就是位置参数,当调用函数时,必须传入参数。

1
2
3
4
5
6
def pwd(x, n):
s = 1
while n > 0 :
n = n - 1
s = s * x
return s

2.默认参数

不难发现上面的函数必须传入两个参数才会正常工作,这个时候我们可以设置默认参数,来实现一些经常的性的操作,比如我们可以将上面的函数的参数n设置成n = 1,从而实现只传入一个参数的时候函数也能很好的工作。

1
2
3
4
5
6
def pwd(x, n = 1):
s = 1
while n > 0 :
n = n - 1
s = s * x
return s

默认参数降低了函数调用的难度,在设置默认参数的时候需要注意的是首先必选参数在前,默认参数在后,其次,当函数有多个默认参数的时候,把经常需要变化的放大前面,变化小的参数放到后面。需要特别注意的就是默认参数 必须指向不变对象

函数默认参数也可以不按照参数顺序进行提供参数,当不按顺序提供默认参数时,需要将参数名写上

3.可变参数*args

*args是可变参数,args接受的是一个touple,重点在于*,后面的args相当于一个变量名,可以自己定义。可变参数的本质就是收集传入的参数集中转变成为元组。
举个栗子:

1
2
3
4
5
6
def changeble(*args):
print(args)

if __name__ == "__main__":
changeble(1, 2, 3)
changeble(1, 2, 3, "hello")

输出结果:

1
2
3
>>>
(1, 2, 3)
(1, 2, 3, "hello")

4.关键字参数

关键字参数**kw可变参数允许你传入0个或者任意个参数,这些可变参数在函数调用时自动组装成为一个tuple,而关键字参数允许传入0个或者任意个含参数名的参数,这些关键字参数在函数内部自动组装成为一个dict

1
2
3
4
5
def keyWord(**kw):
print(kw)

if __name__ == "__main__":
keyWord(name = 'xiaoqiang', age = 23)

输出结果:

1
2
>>>
{'name':'xiaoqiang', 'age':23}

对于关键字参数,你可以传入不受限制的关键字参数,至于最终传入了哪些,就需要在函数的内部进行检查。如果要限制关键字参数的名字,就可以用命名关键字参数,比如,只接受city, job作为关键字参数,可以如下定义:

1
2
def person(name, age, *, city, job):
print(name, age, city, job)

和关键字参数 **kw 不同的是,命名关键字参数需要一个特殊的分隔符 ** 后面的参数被视为命名关键字参数

调用方式如下:

1
2
>>> person('胖子大叔', 25, city = 'Wuhan', job = 'SoftWare Engineer') 
胖子大叔 25 Wuhan SoftWare Engineer

命名关键字参数必须传入参数名,这个和位置参数不同,如果没有传入参数名,调用将会触发异常

5.参数组合

在Python中定义的函数,可以用必选参数,默认参数,可变参数,关键字参数和命名关键字参数,这几种参数可以组合使用,但是一定要注意使用顺序:必选参数,默认参数,可变参数,命名关键字参数和关键字参数

了解了函数的参数,现在看看怎样实现将函数作为返回值,先看一个可变参数的求和:

1
2
3
4
5
def cal_sum(*args):
total = 0
for x in args:
total += x
return total

但是如果不想立即进行求和操作而是根据需要在进行求和这个时候就需要返回求和函数:

1
2
3
4
5
6
7
def return_sum(*args):
def cal_sum():
total = 0
for x in args:
total += x
return total
return cal_sum

当我们调用return_sum()时,返回的不是求和结果,而是求和函数,只有调用函数return_func时,才是真正的求和结果。

1
2
3
4
>>> return_func = return_sum(1, 2, 3)
<function return_sum.<locals>.cal_sum at 0x7f52211ca950>
>>> return_func()
6

在上面的例子中,我们在函数return_sum()中定义了一个求和函数cal_sum(),内部函数可以使用外部函数的参数和局部变量,当外部函数返回内部函数时,先关参数和变量都保存在返回函数中,这种程序结构称之为闭包
需要注意的是返回函数在其定义的内部引用了外部函数的局部变量args,所以当一个外部函数返回了一个内部函数后其局部变量还被返回的内部函数所引用这个一定是非常需要注意的。比如:

1
2
3
4
5
6
7
8
9
def counter():
temp = []
for i in range(1, 4):
def pwd():
return i * i
temp.append(pwd)
return temp

f1, f2, f3 = counter()

上面的函数中,每次循环都会创建一个新的函数,然后,将创建的新的函数都返回了。按照正常情况f1(), f2(), f3()输出的结果应该是 1, 4, 9
实际输出结果:

1
2
3
4
5
6
>>> f1()
9
>>> f2()
9
>>> f3()
9

原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9
如果一定要使用循环变量的话,可以在在创建一个函数,将循环变量当前的值计算出来绑定到参数上,这样,当循环变量发生改变的时候,之前的变量已经被绑定到函数参数中,无法被改变。

1
2
3
4
5
6
7
8
9
10
def counter():
def pwd(j):
def cal():
return j * j
return cal

temp = []
for i in range(1, 4):
temp.append(pwd(i)) # 此时pwd()函数会被执行,从而计算出变量的结果
return temp

匿名函数

当传输的函数不需要显式的定义函数时,可以使用匿名函数。关键字lambda表示匿名函数,冒号前面的x表示函数参数。匿名函数有个限制就是只能写一个表达式,不能写return,返回值就是该表达式的结果。相比于定义函数,匿名函数不会产生函数名冲突,同时匿名函数也是一个函数对象,也可以将匿名函数赋值给一个变量,再利用变量来调用该函数。

1
2
f = lambda x: x*x
f(5)

装饰器

函数是一个对象,而且函数的对象还可以赋值给变量,从而通过变量也可以调用该函数。

1
2
3
4
5
def now():
print('1993-9-3')

f = now() # 赋值给常量
f() # 调用函数

现在,需要在不修改原来函数的情况下增强原来函数的功能,这种的动态增加函数功能的方式,就是“装饰器”(decorator),本质上,decorator就是一个返回函数的高阶函数。
先定义一个打印日志的decorator:

1
2
3
4
5
def log(func):
def wrapper(*args, **kw):
print('call {}'.format(func.__name__))
return func(*args, **kw)
return wrapper

使用@语法将decorator放置到函数定义处:

1
2
3
@log
def now():
print('1993-9-3')