7.1 又见函数
本节着重介绍函数式编程(Functional Programming)
1.python中的函数式
至今我们经历了两种编程范式:面向过程编程与面向对象编程。
面向过程编程利用选择和循环结构以及函数,模块等,对指令进行封装。
面向对象编程实现了另一种形式的封装,包含有数据的对象的一系列方法,这些方法能造成对象的改变。
作为第三种编程范式,函数式编程的本质也在于封装。
函数式编程:正如其名,函数式封装是以函数为中心进行代码封装。函数具有参数和返回值,分别起到输入与输出数据的功能。函数式编程强调了函数的纯粹性(purity),一个纯函数是没有副作用(side effect)的,即这个函数的运行不会影响其他函数。纯函数像一个沙盒,把函数带来的效果都控制在内部,从而不影响程序的其他部分。为了达到纯函数的标准,函数是编程要求其变量都是不可变的。
python并非完全是函数式编程语言,在python中,存在着可变更对象,也能写出非纯函数。但如果我们借鉴函数式编程,尽量在编程中避免副作用,就会有许多好处。由于纯函数相对独立,我们不必担心函数调用对其他函数的影响,所以使用起来更加简单。另外,纯函数也方便进行并行化运算,在并行化编程期间,我们经常担心不同进程之间相互干扰的问题。
当多个进程同时修改一个变量时,进程的先后顺序会影响最终结果。
from threading import Thread
x = 5
def double():
global x
x = x*2
def plus_ten():
global x
x = x + 10
thread1 = Thread(target=double)
thread2 = Thread(target=plus_ten)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(x) #打印20或者30
上面这个函数中使用了全局变量global。global说明x是一个全局变量,函数对全局变量的修改能被其他函数看到,因此有副作用。如果两个函数并行地执行两个函数,函数的执行顺序不确定,结果可能是double中的x*2先执行(20),或者是plus_ten中的x+10先执行(30),这被称为竞跑条件(race condition)。
函数式编程消灭了副作用,即无形中消灭了竞跑条件的可能。因此函数式编程天生地适用于并行化运算。
2.并行运算
并行运算:多条指令同时执行,大规模的并行运算通常是在有多个主机组成的集群(cluster)上进行的;
串行运算:每次执行一条指令,多见于单处理器计算机。
※一个集群造价不菲,我们也可以在单机上通过多进程多线程的方式模拟多个主机的并行处理。
进程:即使在一台单机中,也存在多个运行着的程序,即所谓的进程。一个程序运行后就成为一个进程,进程有自己的内存空间,用来存储自身的运行状态,数据和相关代码。一个进程一般不会直接读取其他进程的内存空间,进程运行期间,可以完成程序描述的工作。
※事实上,单机的处理器按照‘分时复用’的方式,把运算能力分配给多个进程,处理器在进程中来回切换,也能造成多个进程齐头并进的效果。从这个角度来说,集群和单机都实现了多个进程的并行运算,只不过集群的多进程分布在不同的主机上,单机用‘分时复用’的方法实现多进程并行运算。
以下是多进程编程的一个例子:
import multiprocessing
def proc1():
return 999*999
def proc2():
return 888*888
p1 = multiprocessing.Process(target = proc1)
p2 = multiprocessing.Process(target = proc2)
p1.start()
p2.start()
p1.join()
p2.join()
上面程序用来两个进程,进程的工作包含在函数中,分别是proc1与proc2.
方法start()用于程序的启动,join()用于在主程序中等待相应的进程完成。
线程:在一个进程内部,又有多个称为‘线程’的任务,处理器可以在多个线程之间切换,从而形成并行的多线程处理。线程与进程类似,但线程之间可以共享同一个进程内存空间。
7.2 被解放的函数
1.函数作为参数和返回值
再函数式编程中,函数是第一级对象,即函数能像普通对象一样使用。对于一切皆对象的函数来说这是自然而然地结果。既然如此,函数可以像一个普通对象一样成为其他函数的参数:
def square_sum(a,b):
return a**2+b**2
def cubic_sum(a,b):
return a**3+b**3
def argument_demo(f,a,b):
return f(a,b)
print(argument_demo(square_sum,3,5)) #打印34
print(argument_demo(cubic_sum,3,5)) #打印152
上面的程序里,函数square_sum()和cubic_sum()就充当了函数argument_demo()的第一个参数。
很多编程语言都把函数作为参数使用,例如C语言。再图形化界面编程时,这样一个作为参数的函数经常起到回调(callback)的作用——当某个事件发生时,比如界面上一个按钮被按下,回调函数就会被调用:
import tkinter as tk
def callback():
'''callbackfunction for button click'''
listbox.insert(tk.END,'HELLO')
if __name__ == '__main__':
master = tk.Tk()
button = tk.Button(master,text = 'OK',command = callback)
button.pack()
listbox = tk.Listbox(master)
listbox.pack()
tk.mainloop()
python中内置了tkinter图形化功能,再上面的程序中,回调函数将在列表栏中插入‘HELLO’。回调函数作为参数传给按钮构造器,每当按钮被点击时,回调函数就会被调用。
2.函数作为返回值
既然函数可以作为一个对象,那也可以成为另一个函数的返回结果:
def line_conf():
def line(x):
return 2*x+1
return line #retuen a function object
my_line = line_conf()
print(my_line(5)) #打印11
上面的函数可以看到一个内部定义的函数,和函数的内部对象一样,函数对象也有存活范围,也就是函数对象的作用域,而python的缩进很容易让我们看到函数对象的作用域——函数对象的作用域与它的def的缩进层级相同:
def line_conf():
def line(x):
return 2*x+1
print(line(5)) #打印11
if __name__ == '__main__':
line_conf()
print(line(5)) #作用域外,并报错
函数line()定义了一条直线(y=2x+1),在line_conf()中可以调用line()函数,在作用域之外调用line()函数就会有以下错误:
说明这已经超出了函数line()的作用域范围,python对该函数的调用失败。
3.闭包
上一个函数中,line()定义嵌套在另一个函数内部,如果函数的定义中引用了外部变量,会发生什么呢:
def line_conf():
b = 15
def line(x):
return 2*x + b
b = 5
return line
if __name__ == '__main__':
my_line = line_conf()
print(my_line(5)) #打印15
可以看到,line()定义的隶属程序块中引用了高层级的变量b,b的定义并不在line()的内部,而是一个外部对象,我们把b成为line()的环境变量,尽管b定义在line()的外部,但当line()被函数line_conf()返回时,还是会带有b的信息。
概念:一个函数和它的环境变量合在一起,就会构成一个闭包(closure)。
上面程序中,b分别在line()前后各有一次不同的赋值,但line()取值是b=5,说明闭包中包含的是内部函数返回时的外部对象的值。
在python中,闭包是一个包含有环境变量取值的函数对象,环境变量被复制到函数对象的__closure__属性中:
def line_conf():
b = 15
def line(x):
return 2*x + b
b = 5
return line
if __name__ == '__main__':
my_line = line_conf()
print(my_line.__closure__)
print(my_line.__closure__[0].cell_contents)
打印:
可以看到,my_line()的__closure__属性中包含了一个元组,这个元组中每个元素都是cell类型对象,第一个cell包含的就是整数5,也就是我们返回的闭包时的环境变量b的取值。
闭包可以提高代码的可复用性:
def line1(x):
return x + 1
def line2(x):
return 4*x + 1
def line3(x):
return 5*x + 10
def line4(x):
return -2*x - 6
可以把这个程序改为:
def line_conf(a,b):
def line(x):
return a*x+b
return line
line1 = line_conf(1,1)
line2 = line_conf(4,1)
line3 = line_conf(5,10)
line4 = line_conf(-2,-6)
这个例子中,函数line()与环境变量a,b构成闭包,在创建闭包时,我们通过line_conf()的参数a,b来说明直线的参量。这样就能复用一个闭包,通过带入不同的参数来获得不同的直线函数。
除了复用代码,闭包还能起到减少函数参数的作用:
def curve_closure(a,b,c):
def curve(x):
return a*x**2+b*x+c
return curve
curve1 = curve_closure(1,2,1)
curve()是一个二次函数,他除自变量x外,还有a,b,c三个参数,通过curve_closure()这个闭包,我们可以预设a,b,c三个参数的值,从而起到减参的目的。
7.3 装饰器
1.装饰器介绍
概念:装饰器(decorator)是一种高级python语法,装饰器可以对一个函数,方法或者类进行加工。
我们先定义两个简单的数学函数,一个平方和一个平方差:
def square_sum(a,b):
return a**2+b**2
def square_diff(a,b):
return a**2-b**2
if __name__ == '__main__':
print(square_sum(3,4)) #打印25
print(square_diff(3,4)) #打印-7
在拥有基本的数学功能之后,我们想为函数加上其他功能,比如打印输入:
def square_sum(a,b):
print('input:',a,b)
return a**2+b**2
def square_diff(a,b):
print('input:',a,b)
return a**2-b**2
if __name__ == '__main__':
print(square_sum(3,4))
print(square_diff(3,4))
两个函数定义中都增加了语句‘’print('input:',a,b)‘’这个打印功能,改用装饰器定义拓展本身再把装饰器用于两个函数:
def decorator_demo(old_fuction):
def new_function(a,b):
print('input:',a,b)
return old_function(a,b)
return new_function
@decorator_demo
def square_sum(a,b):
return a**2+b**2
@decorator_demo
def square_diff(a,b):
return a**2-b**2
if __name__ == '__main__':
print(square_sum(3,4))
print(square_diff(3,4))
装饰器可以用def定义,如上面代码中的decorator_demo(),装饰器接受一个可调用对象作为输入参数,并返回一个新的可调用对象。装饰器新建了一个函数对象,也就是上例中的new_function(),在new_function()中我们增加了打印功能,并通过调用old_function()来保留原有的函数功能。
※定义好装饰器后,我们就可以通过@语法使用装饰器了。在定义函数square_sum()和square_diff()之前调用@decorator_demo,实际上是将square_sum()或square_diff()传递给了decorator_demo,并将decorator_demo返回的新函数对象赋给原来的函数名square_sum()或square_diff()。
举个栗子,当我们调用square_sum(3,4)时,实际上发生的是:
square_sum = decorator_demo(square_sum)
square_sum(3,4)
我们知道,python中对象与变量名是分离的,变量名是指向一个对象的引用。从本质上,装饰器起到的作用就是名称绑定(name binding),让同一个变量名指向一个新返回的函数对象,从而达到修改函数对象的目的。只不过我们很少彻底改变函数对象,在使用装饰器时,我们往往会在新函数内部调用旧函数,以便保留旧函数的功能,这也是‘装饰’名称的由来。
接下来写一个更实用的装饰器。我们用time包来测量程序运行的时间,把测量程序运行时间的功能做成一个装饰器,将这个装饰器用于其他函数,将显示函数实际运行的时间:
import time
def decorator_timer(old_function):
def new_functiong(*arg,**dict_arg):
t1 = time.time()
result = old_function(*arg,**dict_arg)
t2 = time.time()
print('time is:',t2-t1)
return result
return new_function
在new_function()中,除调用旧函数之外,还前后各调用了一次time.time(),由于time.time()返回挂钟时间,他们的差值反应了旧函数的运行时间。
2.带参装饰器
在之前的装饰器调用中,比如@decorator_demo中,该装饰器默认它后面的函数是唯一的参数。装饰器语法允许我们调用decorator时提供其他参数,比如@decorator(a),这样能提高编程灵活性:
def pre_str(pre=''):
def decorator(old_function):
def new_function(a,b):
print(pre+'input:',a,b)
return old_function(a,b)
return new_function
return decorator
#装饰square_sum()
@pre_str('*^_^*')
def square_sum(a,b):
return a**2+b**2
#装饰square_diff
@pre_str('┭┮﹏┭┮')
def square_diff(a,b):
return a**2-b**2
if __name__ == '__main__':
print(square_sum(3,4))
print(square_diff(3,4))
3.装饰类
除了装饰函数,装饰器还能作用于类,它可以接受一个类,返回一个类,从而起到加工类的效果:
def decorator_class(SomeClass):
class NewClass(object):
def __init__(self,age):
self.total_display = 0
self.wrapped = SomeClass(age)
def display(self):
self.total_display += 1
print('total_display',self.total_display)
self.wrapped.display()
return NewClass
@decorator_class
classBird(object):
def __init__(self,age):
self.age = age
def display(self):
print('My age is:',self.age)
if __name__ == '__main__':
eagle_lord = Bird(5)
for i in range(3):
eagle_lord.display()
7.4 高阶函数
1.lambda与map()
在之前的内容都在讲一个中心思想:函数能像一个普通对象一样应用,成为其他函数的参数或者返回值。那么,能接受其他函数作为参数或返回值的函数被称为高阶函数(high-order function),通俗理解,高阶函数就是能处理函数的函数。
本节介绍最具代表性的高阶函数:map(),filter(),reduce()
在此之前,有一种新的函数定义方法需要了解。除了def方式定义函数,我们还能使用另一方式定义函数,那就是lambda语句定义匿名函数:
lambda_sum=lambda x,y:x+y
print(lambda_sum(3,4)) #打印7
通过lambda我们创建了一个匿名函数对象,借着赋值语句这个匿名函数被赋予了函数名lambda_sum。函数的参数为x,y。返回值为x+y。函数lambda_sum()的调用和正常函数一样,这种用lambda来产生匿名函数的方式适用于简短的函数定义。
函数map()是python的内置函数,它的第一个参数就是一个函数对象,函数map()把这一个函数对象作用于多个元素:
data_list = [1,3,5,6]
result = map(lambda x:x+3,data_list)
函数map()第二个参数是一个可循环对象。对于data_list的每一个元素,lambda函数都会调用一次,那个元素会成为lambda函数的参数,换句话说,map()会把接收到的函数对象依次作用于每一个元素。最终map()会返回一个迭代器,迭代器中的元素就是多次调用lambda函数的结果。
def equivalent_generator(func,iter):
for item in iter:
yield func(item)
data_list = [1,3,5,6]
result = map(lambda x:x+3,data_list)
上面的lambda函数只有一个参数,这个函数也可以是一个多参数的函数,这时就要求map()的参数列表中提供相应数目的可循环对象:
def square_sum(x,y):
return x**2+y**2
data_list1 = [1,3,5,7]
data_list2 = [2,4,6,8]
result = map(square_sum,data_list1,data_list2)
这里,square_sum()作为map()的第一个参数,函数square_sum()需要两个可循环对象,第一个循环对象提供了square_sum()中对应的x参数,第二个循环对象提供了square_sum()中对应的y参数,他们的关系如图:
一定程度上map()可以替代循环的功能,用map()写函数也更加简洁。
2.filter函数
与map()函数一样,内置函数filter()函数的第一个参数也是一个函数对象,它也将这个函数对象作用域可循环对象的多个元素。如果函数对象返回的是True,则该次的元素被放到返回的迭代器中,也就是说,filter()通过调用函数来筛选数据:
def larger100(a):
if a > 100:
return True
else:
return False
for item in filter(larger100,[10,56,101,500]):
print(item) #打印101 500
上面是一个使用filter()函数的例子,作为参数的larger100()函数用于判断列表中元素是否比100大。
类似的,filter()用于多参数函数时也可以在参数中增加更多的可循环对象。
3.reduce函数
reduce()函数在标准库的functools包中,使用之前需要引入。和map(),filter()一样,reduce()函数的第一个参数是函数。但是reduce()对作为参数的函数对象有特殊的要求,那就是这个作为参数的函数对象必须能接受两个参数。reduce()能把函数的对象累进地作用于各个参数:
from functools import reduce
data_list = [1,2,5,7,9]
result = reduce(lambda x,y:x+y,data_list)
print(result) #打印24
函数reduce()的运作过程可以归纳成下图:
在并行运算中,Reduce运算紧接着Map运算,map运算的结果分布在多个主机上,reduce运算把结果都收集起来,因此,谷歌用于并行运算的软件架构,就称为MapReduce。
4.并行处理
import time
from multiprocessing import Pool
import requests
def decorator_timer(old_function):
def new_function(*arg,**dict_arg):
t1 = time.time()
result = old_function(*arg,**dict_arg)
t2 = time.time()
print('time:',t2-t1)
return result
return new_function
def visit_once(i,address='http://www.cnblogs.com'): #1单位下载过程函数
r = requests.get(address)
return r.status_code
@decorator_timer
def single_thread(f,counts):
result = map(f,range(counts))
return list(result)
@decorator_timer
def multiple_thread(f,counts,process_number=10): #启动10个进程
p = Pool(process_number)
return p.map(f,range(counts))
if __name__ == '__main__':
TOTAL = 100
print(single_thread(visit_once,TOTAL))
print(multiple_thread(visit_once,TOTAL))
7.5 自上而下
1.便捷表达式
在书上的开头,我们就提到函数式编程的思维是自上而下的,python中也有不少语法体现了这一点,如生成器表达式,列表解析,词典解析。
生成器表达式是构建生成器的便捷方法:
def gen():
for i in range (4):
yield i
等价地,上面的程序可以写成——生成器表达式(Generator Expression):
gen = (x for x in range(4))
再来看生成一个列表的方法:
l = []
for x in range(10):
l.append(x**2)
print(l) #打印[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
上述代码生成了一个表,但又更快的方法——列表解析(List Comprehension):
l = [x**2 for x in range(10)]
print(l) #打印[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
列表解析的语法更简洁,在语句中直截了当的说想要的是元素的平方,然后再通过for来增加限定条件,即哪些元素的平方。除了for,列表解析还可以用if:
x1 = [1,3,5]
y1 = [9,12,13]
l = [x**2 for (x,y) in zip(x1,y1) if y > 10]
print(l) #打印9 25
词典解析可用于快捷地生成词典:
d = {k: v for k,v in enumerate('Vamei') if val not in 'Vi'}
2.懒惰求值
迭代器的元素都是实时计算出来的,在使用该元素之前,元素并不会占据内存空间,与之相对应的,列表在建立时就已经产生了各个元素的值,并保存在内存中。迭代器的工作方式正式函数式编程中的‘懒惰求值(Lazy Evaluation)’。我们可以对迭代器进行各式各样的操作,但只有在需要时,迭代器才会计算出具体的值。懒惰求值护最小化计算机要做的工作,例如:
a = range(100000000)
result = map(lambda x:x**2,a)
这个程序在python3中能飞速完成,因为map()返回的是迭代器,所以会懒惰求值。除非通过某种方式调用迭代器中的元素,或者把迭代器转化为列表,运算过程才会开始。因此,在下面的程序中,如果把结果转化为列表,那么运算时间将会大大增加:
a = range(100000000)
result = map(lambda x:x**2,a)
result = list(result)
除了运算资源,懒惰求值还能节约内存空间,对即时求值来说,其运算过程的中间结果都需要占用不少内存空间,而懒惰求值可以现在迭代器层面上进行操作,在获得最终迭代器以后一次性完成计算。
3.itertools包
标准库中的intertools包提供了更加灵活生成迭代器的工具。
from itertools import *
count(5,2) #从5开始的整数迭代器,每次增加2 即5,7,9……
cycle('abc') #重复序列元素,即a,b,c,a,b,c,a,b,c……
repeat(1.2) #重复1.2,构成无穷迭代器,即1.2,1.2,1.2,1.2……
repeat(10,5) #重复10,共重复5次
chain([1,2,3],[4,5,6]) #连接两个迭代器,使之变为一个。即1,2,3,4,5,6
product('abc',[1,2]) #多个迭代器集合的笛卡儿积,相当于嵌套循环
所谓的笛卡儿积可以得出集合元素所有可能的组合方式:
from itertools import *
for m,n in product('abc',[1,2]):
print(m,n,',') #打印a1,a2,b1,b2,c1,c2
permutations('abc',2) #从abc中挑选两个元素,比如ab,bc……将所有的结果排序,返回新的迭代器
#上面返回的组合是有顺序区分的,比如ab,ba都会返回
combinations('abc',2) #从abc中挑选两个元素,比如ab,bc……将所有的结果排序,返回新的迭代器
#上面的组合不区分顺序,即ab与ba只会返回一个ab
combinations_with_replacement('abc',2) #与上面两种类似,但是允许元素的重复,即aa,bb,cc
itertools还提供了很多有用的高阶函数:
starmap(pow,[(1,1),(2,2),(3,3)]) #pow将依次作用于表的每一个tuple
takewhile(lambda x:x<5,[1,3,6,7,1])
#当函数返回True时,收集元素到迭代器,一旦函数返回False则停止。1,3
dropwhile(lambda x:x>5,[1,3,6,7,1])
#当函数返回False时,跳过元素,一旦函数返回True,则开始收集剩下的元素到迭代器。6,7,1
如果有一个迭代器,包含一群人的 身高,我们可以使用这样一个key()函数:如果身高大于180,返回‘tall’,如果身高小于160,返回‘short’,如果在两者之间,返回‘middle’。最终所有的身高将分为三个迭代器,即‘tall’,‘middle’,‘short’:
from itertools import groupby
def height_class(h):
if h > 180:
return 'tall'
elif h <160:
return 'short'
else:
return 'middle'
friends = [190,158,159,165,170,177,181,182,190]
friends = sorted(friends,key = height_class)
for m,n in groupby(friends,key = height_class):
print(m)
print(list(n))
程序中使用了group()函数,它能将一个key()函数作用于原迭代器的各个元素,从而获得各个函数的键值。然后根据key()函数的结果,将所有元素分组。
打印:
其中,groupby()函数的功能类似于Unix中的uniq命令,分组之前需要使用sorted()函数进行排序,根据key()函数排序,让同组元素先在位置上靠拢。
这个包中还有一些其他的工具,方便迭代器构建:
compress('ABCD',[1,1,1,0]) #根据[1,1,1,0]的真假值情况选择保留第一个参数中的元素。A,B,C
islice() #类似于slice()函数,只是它返回的是一个迭代器
izip() #类似于zip()函数,只是它返回的是一个迭代器