Python3之函数式编程
高阶函数
高阶函数英文叫Higher-order function。
什么是高阶函数?
我们以实际代码为例子,一步一步深入概念。
以Python内置的求绝对值的函数abs()为例,
调用该函数用以下代码:
>>> abs(-10)
10
但是,如果只写abs呢?
>>> abs
<built-in function abs>
可见,abs(-10)是函数调用,而abs是函数本身。
如果把函数本身赋值给变量呢?
>>> f = abs
>>> f
<built-in function abs>
结论:函数本身也可以赋值给变量,即:变量可以指向函数。
如果一个变量指向了一个函数,
那么,是否可以通过该变量来调用这个函数?
用代码验证一下:
>>> f = abs
>>> f(-10)
10
成功!说明变量f现在已经指向了abs函数本身。
直接调用abs()函数和调用变量f()完全相同。
函数名也是变量
那么函数名是什么呢?
函数名其实就是指向函数的变量!
对于abs()这个函数,完全可以把函数名abs看成变量,
它指向一个可以计算绝对值的函数!
如果把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这个变量已经不指向求绝对值函数而是指向一个整数10!
这个例子说明函数名也是变量。
要恢复abs函数,请重启Python交互环境。
传入函数
既然变量可以指向函数,函数的参数能接收变量,
那么一个函数就可以接收另一个函数作为参数,
这种函数就称之为高阶函数。
【函数的参数也可以是一个函数】
一个最简单的高阶函数:
def add(x, y, f):
return f(x) + f(y)
当我们调用add(-1, 2, abs)时,参数x,y和f分别接收-1,2和abs,
根据函数定义,我们可以推导计算过程为:
x = -1
y = 2
f = abs
f(x) + f(y) ==> abs(-1) + abs(2) ==> 3
返回函数
函数返回值
python就可以返回int,str,list,dict等类型数据,
python还支持返回函数。
高阶函数除了可以接受函数作为参数外,
还可以把函数作为结果值返回。
python返回函数
基本语法:
def f():
print ('call f()...')
# 定义函数h:
def h():
print( 'call h()...')
# 返回函数h:
return h
这里,最外层的函数f会返回一个函数h,也就是函数h本身;
x = f() # 调用f()
print(x) # 变量x就是上面一个最外层函数返回的函数(不是函数值)
x() # x指向函数,因此可以调用,这里就是调用内层函数h()的过程
只返回函数的作用
返回函数可以把一些计算延迟执行。
如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?
可以不返回求和的结果,而是返回求和的函数:
例如,如果定义一个普通的求绝对值函数:
def calc_abs(lst):
return abs(lst)
>>>calc_abs(-10)
结果直接是:10
但是,可以通过返回函数思想写代码,就可以“延时计算”
def calc_abs(lst):
def lazy_abs():
return abs(lst)
return lazy_abs
下面是调用:
注意,下面代码并没有对函数进行执行计算出结果,而是返回函数。
>>> f = calc_abs(-10)
>>> f
<function lazy_sum at 0x1037bfaa0>
1
2
3
对返回的函数进行调用时,才计算出结果
>>>f()
10
闭包
闭包的概念
闭包(Closure)是词法闭包(Lexical Closure)的简称,
是引用了自由变量的函数。
这个被引用的自由变量将和这个函数一同存在,
即使已经离开了创造它的环境也不例外。
所以,闭包是由函数和与其相关的引用环境组合而成的实体。
闭包预览
以下是一段简单的闭包代码示例:
def foo():
m = 3
n = 5
def bar():
a = 4
return m+n+a
return bar
>>>bar =foo()
>>>bar()
12
说明:
bar在foo函数的代码块中定义。
我们称bar是foo的内部函数。
在bar的局部作用域中可以直接访问foo局部作用域中定义的m、n变量。
简单的说,
内部函数可以使用外部函数变量的行为,就叫闭包。
怎么创建闭包
在Python中创建一个闭包可以归结为以下三点:
- 闭包函数必须有内嵌函数
- 内嵌函数需要引用该嵌套函数上一级函数中的变量
- 闭包函数必须返回内嵌函数
闭包案例一
注意到返回的函数在其定义内部引用了局部变量args,
所以,当一个函数返回了一个函数后,
其内部的局部变量还被新函数引用。
另一个需要注意的问题是,返回的函数并没有立刻执行,
而是直到调用了f()才执行。
我们来看一个例子:
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()
在上面的例子中,
每次循环,都创建了一个新的函数,
然后,把创建的3个函数都返回了。
你可能认为调用f1(),f2()和f3()结果应该是1,4,9,
但实际结果是:
>>> f1()
9
>>> f2()
9
>>> f3()
9
全部都是9!
原因就在于返回的函数引用了变量i,但它并非立刻执行。
等到3个函数都返回时,
它们所引用的变量i已经变成了3,因此最终结果为9。
返回闭包时牢记的一点就是:
返回函数不要引用任何循环变量,
或者后续会发生变化的变量。
闭包案例二
如果一定要引用循环变量怎么办?
方法是再创建一个函数,
用该函数的参数绑定循环变量当前的值,
无论该循环变量后续如何更改,已绑定到函数参数的值不变。
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
闭包小结
- 一个函数可以返回一个计算结果,也可以返回一个函数。
- 返回一个函数时,牢记该函数并未执行,
- 返回函数中不要引用任何可能会变化的变量。
闭包(closure)是函数式编程的重要的语法结构,Python也支持这一特性,下面就开始介绍Python中的闭包。
在开始介绍闭包之前先看看Python中的namespace。
Python中的namespace
Python中通过提供 namespace 来实现重名函数/方法、变量等信息的识别,其一共有三种 namespace,分别为:
local namespace: 作用范围为当前函数或者类方法
global namespace: 作用范围为当前模块
build-in namespace: 作用范围为所有模块
当函数/方法、变量等信息发生重名时,Python会按照 “local namespace -> global namespace -> build-in namespace”的顺序搜索用户所需元素,并且以第一个找到此元素的 namespace 为准。
同时,Python中的内建函数locals()和globals()可以用来查看不同namespace中定义的元素。
匿名函数
匿名函数的概念
当我们在传入函数时,
有些时候,不需要显式地定义函数,直接传入匿名函数更方便。
在Python中,对匿名函数提供了有限支持。
还是以sum(x,y)函数为例,计算sum(x,y)=x+y时,
除了定义一个sum(x,y)的函数外,还可以直接传入匿名函数:
>>> m=(lambda x,y: x + y)
>>> print(m(2,3))
通过对比可以看出,匿名函数lambda x,y: x + y实际上就是:
def sum(x,y):
return x + y
关键字lambda表示匿名函数,冒号前面的x,y表示函数参数。
匿名函数的特点
匿名函数有个限制,就是只能有一个表达式,
不用写return,返回值就是该表达式的结果。
用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。
匿名函数赋值给变量
此外,匿名函数也是一个函数对象,
也可以把匿名函数赋值给一个变量,
再利用变量来调用该函数:
>>> f = lambda x: x * x
>>> f
<function <lambda> at 0x101c6ef28>
>>> f(5)
25
匿名函数作为返回值
同样,也可以把匿名函数作为返回值返回,比如:
def build(x, y):
return lambda: x * x + y * y
装饰器
装饰器的概念
装饰器本质上是一个Python函数,
它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,
装饰器的返回值也是一个函数对象。
它经常用于有切面需求的场景,
比如:插入日志、性能测试、事务处理等场景。
装饰器 是解决这类问题的绝佳设计,有了装饰器,
就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
概括的讲,
装饰器的作用就是为已经存在的函数或对象添加额外的功能。
怎么写一个装饰器
def debug(func):
def wrapper():
print("[DEBUG]: enter {}()".format(func.__name__))
return func()
return wrapper
@debug #Python中支持@语法糖
def say_hello():
print ("hello!")
注意:函数对象有一个__name__属性,可以拿到函数的名字
传递一个参数
以上代码是最简单的装饰器,
但是有一个问题:
如果被装饰的函数需要传入参数,那么这个装饰器就坏了。
因为返回的函数并不能接受参数,
这时可以指定装饰器函数wrapper接受和原函数一样的参数,比如:
def debug(func):
def wrapper(something): # 指定一毛一样的参数
print "[DEBUG]: enter {}()".format(func.__name__)
return func(something)
return wrapper # 返回包装过函数
@debug
def say(something):
print "hello {}!".format(something)
传递任意参数
这样你就解决了一个问题,但又多了N个问题。
因为函数有千千万,你只管你自己的函数,
别人的函数参数是什么样子,无法知晓?
幸好,Python提供了可变参数*args和关键字参数**kwargs,
有了这两个参数,装饰器就可以用于任意目标函数了。
def debug(func):
def wrapper(*args, **kwargs): # 指定宇宙无敌参数
print("[DEBUG]: enter {}()".format(func.__name__))
print('Prepare and say...',)
return func(*args, **kwargs)
return wrapper # 返回
@debug
def say(something):
print("hello {}!".format(something))
装饰器小结
在面向对象(OOP)的设计模式中,decorator被称为装饰模式。
OOP的装饰模式需要通过继承和组合来实现,
而Python除了能支持OOP的decorator外,
直接从语法层次支持decorator。
偏函数
Python的设计核心原则就是简洁:
在这种原则的指导下,诞生了lambda表达式和偏函数:
二者都让函数调用变得简洁。
接下来介绍偏函数的应用。
偏函数的概念
如果我们定义了一个函数,
比如说将四个数相加add(one ,two,three,four),
上层有很多函数需要调用这个函数。
在这些调用中,80%的调用传递的参数都是one=10,two=20,
如果我们每都输入相同的参数,这样乏味而且浪费,
固然,我们可以通过默认参数来解决这个问题;
但是如果另外我们也需要参数是one=20,two=10的情况呢?
所以,我们需要一种函数,
能够将任意数量的参数的函数转化成带剩余参数的函数对象。
简单的说,偏函数就是某一种函数带有固定参数的实现,
所以,我们需要:
1)给偏函数命名
2)传递固定参数
偏函数的实现
functools.partial可以设置默认参数和关键字参数的默认值
Python的functools模块提供了很多有用的功能,
其中一个就是偏函数(Partial function)。
通过设定参数的默认值,可以降低函数调用的难度,
而偏函数也可以做到这一点。
举例如下:
int()函数可以把字符串转换为整数,
当仅传入字符串时,int()函数默认按十进制转换:
>>> int('12345')
12345
但int()函数还提供额外的base参数,默认值为10。
如果传入base参数,就可以做N进制的转换:
>>> int('12345', base=8)
5349
>>> int('12345', 16)
74565
假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,
于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:
def int2(x, base=2):
return int(x, base)
这样,我们转换二进制就非常方便了:
>>> int2('1000000')
64
functools.partial帮助我们创建了一个偏函数,
不需要我们自己定义int2(),
可以直接使用下面的代码创建一个新的函数int2:
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85
偏函数小结
简单总结functools.partial的作用就是,
把一个函数的某些参数(不管有没有默认值)给固定住(也就是设置默认值),
返回一个新的函数,调用这个新函数会更简单。
最后,创建偏函数时,要从右到左固定参数,
就是说,对于函数f(a1, a2, a3),
可以固定a3,也可以固定a3和a2,
也可以固定a3,a2和a1,
但不要跳着固定,比如只固定a1和a3,把a2漏下了。
当函数的参数个数太多,需要简化时,
使用functools.partial可以创建一个新的函数,
这个新函数可以固定住原函数的部分参数,
从而在调用时更简单。