4函数式编程
函数是面向过程的程序设计的基本基元。而函数式编程-Function Programming,虽然其接近于面向过程的编程,但是更多的是偏于数学计算。
函数式编程是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。
函数式编程的一个特点就是:允许把函数作为一个参数传入另外一个函数,还允许返回一个函数。
Python对函数式编程提供部分支持,由于Python允许使用变量,因此,Python不是纯粹的函数式编程。
4.1高阶函数
高阶函数-Higher-order function。
变量可以指向函数**
以Python的绝对值函数
abs()
为例:>>> abs(-10) 10
但是如果写成:
>>> abs <built-in function abs>
可见
abs(-10)
是函数调用,而abs
是函数本身。要获得函数的调用,我们可以把结果赋值给变量:
>>> x = abs(-10) >>> x 10
但是如果将函数赋值给一个变量:
>>> f=abs >>> f(-10) 10
结论:函数本身可以赋值给变量,即:变量可以指向函数。
- 函数名也是变量
函数名其实就是名副其实的指向函数的变量。当把函数名
abs
指向其他对象:>>> abs = 10 >>> abs(-10) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object is not callable
可见,把
abs
指向10后,就无法通过abs(-10)
调用该函数了。注:由于
abs
函数实际上是定义在import builtins
模块中的,所以要让修改abs
变量的指向在其他模块生效,要用import builtins;builtins.abs = 10
- 传入函数
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
def add(x,y,f):
return f(x) + f(y)
例如,调用add(-10,3,abs)
>>>add(-10,3,abs)
13
编写高阶函数就是让函数能接收函数作为参数。
小结:把函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。
4.1.1 map/reduce
Python内建了map()
和reduce()
函数。
map()
函数map()
函数接收两个参数,一个参数是函数,一个参数是Iterable
,map()
将传入的函数依次作用于Iterable
对象的每个元素,结果返回一个Iterator
。例如:
>>> def f(x):
... return x*x
>>> r = map(fun,[1,2,3,4])
>>> r
<map object at 0x00000222A759E780> ##注意,r是一个惰性的序列,无法一次性输出,必须转换成list类型
>>> next(r)
1
>>> next(r)
4
>>> next(r)
9
>>> list(r) ##r的指向改变了,当使用多次next()函数,r指向的不再是原来的Iterator对象。而是类似于栈帧发生位移
[16]
>>> list(r)
[]
>>> list(r)
[]
>>> r = map(fun,[1,2,3,4])
>>> list(r)
[1, 4, 9, 16]
>>>
map()
函数作为高阶函数,可以计算任意复杂的函数,比如把list
转换为一个字符串:
>>> r1 = map(str,[1,2,3,4,5])
>>> list(r1)
['1', '2', '3', '4', '5']
reduce()
函数
reduce()
接收两个参数,一个是*函数*f,一个是Iterable
对象。函数必须接受两个参数,reduce
函数每次会把上次函数允许的结果和Iterable
对象的下一个元素传入到函数f中。直到Iterable
中所有的元素都参与过运算。
>>>from functools import reduce
>>>def testadd(x,y):
return x+y
>>> reduce(testadd,[1,3,5])
9
再来看一个复杂的例子:把字符串”13579”转换成整数:
def str2num(s):
def fn(x,y):
return x*10+y
def char2int(s):
return {'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,'0':0}[s]
return reduce(fn,map(char2int,s))
运行结果:
>>> str2num('222')
222
可以使用lambda
函数进一步简化:
def str2num2(s):
def char2int(s):
return {'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,'0':0}[s]
return reduce(lambda x,y:x*10+y,map(char2ints,s))
4.1.2filter
Python内建了filter
函数用于过滤序列。
和map
类似,filter
函数也可以接收一个函数和一个序列。和map()
不同的是filter()
函数把函数参数作用于序列的每一个元素,判断该元素是否满足函数参数,根据函数参数返回的True
或False
来决定该元素的去和留
例如:过滤掉序列中的空字符串
>>>def Not_empty(s):
... return s and s.strip()
>>>list(filter(Not_empty,['A','B','SSSS',' ','AAAAA',None]))
['A','B','AAAAA']
filter()
的作用是从一个序列中筛出符合条件元素。使用了惰性计算,所以只有在取filter()
结果的时候,才会真正的筛选并每次返回下一个筛除的元素。
4.1.3 sorted
排序算法
Python内置的函数
sorted()
就可以对list
进行排序。>>> sorted([10,-9,3,93]) [-9, 3, 10, 93]
sorted()
函数也是一个高阶函数,他可以接收一个key
函数来实现自定义的排序,例如按绝对值大小排序:
>>> sorted([10,-9,3,93],key=abs)
[3, -9, 10, 93]
key
函数作用于序列的每一个元素,并将结果排序。
对于字符串排序,是按照ASCII的大小进行排序。由于’Z’<’a’,如果我们忽略大小写,对字符串进行排序。如果使用key
函数,把字符串映射为忽略大小写排序即可。
忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较:
>>> sorted(['Asss','ass','ZXZ','bs']) ##字符串排序
['Asss', 'ZXZ', 'ass', 'bs']
>>> sorted(['Asss','ass','ZXZ','bs'],key=str.lower) ##忽略大小写,排序
['ass', 'Asss', 'bs', 'ZXZ']
4.2返回函数
函数作为返回值
高阶函数除了可以接收函数作为参数外,还可以把函数作为结果返回值。
例如,实现一个可变参数的求和
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
当我们调用lazy_sum()
函数时,返回的并不是求和的结果,而是求和函数。调用返回值时才算是真正的求和。
>>> from test1 import lazy_sum
>>> f = lazy_sum(1,2,3,4)
>>> f
<function lazy_sum.<locals>.sum at 0x000001B6A3BD3E18>
>>> f()
10
在这个例子中,我们在函数lazy_sum()
中定义了一个函数sum()
,内部函数可以引用外部函数lazy_sum()
的参数和局部变量,当lazy_sum()
函数返回函数sum
时,相关参数和变量都保存在返回的函数中,这种称为:闭包(Closure)的程序结构拥有极大的威力
请再注意一点:当我们每次调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数:
>>> f2 = lazy_sum(1,2,3,4)
>>> f3 = lazy_sum(1,2,3,4)
>>> f2==f3
False
闭包
注意到返回的函数在其定义内部引用了局部变量
args
,所以,当一个函数返回一个函数后,其内部的局部变量还被函数引用。所以,闭包使用起来方便,实现起来可不容易。
返回闭包时要牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
小结:
一个函数不仅可以返回一个计算结果,也可以返回一个函数。
返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会发生变化的变量。
4.3 匿名函数
当我们传入函数时,有些时候,不需要显示的定义函数,只需要传入匿名函数更方便。在Python中,对匿名函数提供了有限支持。
以map()
函数为例,计算f(x) = x^2
。我们使用匿名函数:
>>> list(map(lambda x:x*x,[1,2,3,4,5,6,7,8]))
[1, 4, 9, 16, 25, 36, 49, 64]
关键字lambda
表示匿名函数,冒号前面的x表示函数参数。
匿名函数有个限制,就是只能有一个表达式,不用写return
语句,返回值就是该表达式的结果。
匿名函数也是一个函数对象,可以把匿名函数赋值给一个变量,再利用该变量来调用函数。
>>> f=lambda x:x*x
>>> f(3)
9
4.4 装饰器
函数对象有个_name_
属性,可以拿到函数的名字。
假如有一个函数,打印当前时间:
import time ##yin
def now():
print(time.strftime("%Y-%m-%d %H:%M:%S",time.localtime())
现在假如我们要增强now()
函数的功能,在函数调用前后都自动打印日志,但又不希望更改函数now()
的定义,这种在代码运行时动态增加功能的方式,称之为“装饰器”(Decorator)
本质上,Decorator就是一个返回函数的高阶函数。我们如果要定义一个可以打印日志的“装饰器”:
def log(func):
def wrapper(*args,**kw):
print("call %s():" % func._name_)
return func(*args,**kw)
return wrapper
log()
函数,是一个decorator
,可以接收一个函数作为参数。我们可以借助Python的@语法,把decorator
置于函数的定义处:
@log
def now():
print(time.strftime("%Y-%m-%d %H:%M:%S",time.localtime())
调用now()
函数,不仅会允许now()
函数本身,还会在运行now()
函数前打印一行日志。
由于log()
函数,是一个decorator
,返回一个函数,因此原来now()
函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()
函数将执行新的函数,即log()
函数中返回wrapper(*args,**kw)
函数。
wrapper(*args,**kw)
函数的参数定义是(*args,**kw)
,因此,该函数可以接受任意参数的调用。在wrapper(*args,**kw)
函数内,首先打印日志,再紧接着调用原始函数。
如果decorator
本身需要传入参数,那么就需要编写一个返回decorator
的高阶函数,写出来会更复杂:
def log(text):
def decorator(func):
def wrapper(*args,**kw):
print("call %s():" % func._name_)
return func(*args,**kw)
return wrapper
return decorator
这个三层嵌套的decorator
用法如下:
@log('execute')
def now():
print(time.strftime("%Y-%m-%d %H:%M:%S",time.localtime())
执行结果如下:
>>>now()
execute now():
2017-8-25
小结:
在面向对象(OOP)的设计模式中,decorator
被称为装饰模式,OOP的装饰模式需要通过继承和组合来实现,而Python除了支持OOP的decorator
外,直接从语法层次支持decorator
。Python的decorator
可以用函数来实现,也可以用类来实现。
decorator
可以增强函数的功能,定义起来稍微有些复杂,但是用的时候非常灵活方便。
4.5偏函数
Python中的functools模块,提供了许多有用的功能,其中一个就是偏函数(Partial function)。要注意,这里的偏函数和数学意义上的不一样。
Python中的int()
函数可以将字符串转换为整数。默认是十进制的整数,int()
函数还提供额外的base
参数,默认为10:
>>> int('123')
123
>>> int('123',base=8)
83
>>> int('123',base=2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 2: '123'
>>> int('100100',base=2)
36
假设要转换大量的二进制函数,每次都要传入int('123',base=2)
,非常麻烦。可以定义一个默认参数的函数:
def int2(_str,base=2):
return int(_str,base)
如此,我们转换二进制参数就非常方便了,但是代码量还是非常不方便。我们使用functools.partial
,用来创建一个偏函数:
>>> import functools
>>> int2 = functools.partial(int,base=2)
>>> int2('10000100')
132
只需要一行代码,无需重新定义一个函数,上面的代码int2
是偏函数名。functools.partial()
的第一个参数是函数,第二个参数是函数的默认参数。返回值为一个新函数。
该函数仅仅将base=2作为默认函数,也可以使用其他值:
>>>int2('2221111',base=10)
偏函数可以接收函数对象、*args
和**kw
这三个参数。
小结:
当函数参数过多,需要简化时,使用functools.partial()
可以创建一个新函数,这个新函数就可以固定原函数的部分参数,从而在调用时更简单。