1 函数式与多线程
函数式就好比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)
可以看到两个函数同时会影响全局变量x
,如果我们不确定哪个函数先运行完毕,得到的结果就不同,可能是20,也有可能是30。这种不确定性在多线程中成为竟跑条件,在并行中要避免这种情况。因为函数式编程中的函数没有副作用,所以就没有竟跑条件,每个线程都十分安全可靠。
正是因为函数式编程有着多种优点,在程序语言高速发展的今天,它越来越受程序员的青睐。Python在后来(2.7以后),也加入了函数式编程的一些函数和语法。利用Python函数式编程的学习,我们正好可以了解这些思想,也可以更深入的利用这种编程技能,实现自上而下的计算思维与编程体系。
2 纯粹的函数
我们将从多方面了解函数的纯粹。首先来看函数如何作为参数并返回的。Python中一切皆对象,那么函数也可以像变量一般,把它看做对象,在参数和返回值里使用,我们看个例子:
def square_sum(a, b):
return a2+b2
def cubic_sum(a, b):
return a3+b3
def argument_demo(f, a, b):
return f(a, b)
print(argument_demo(square_sum, 3, 4))
print(argument_demo(cubic_sum, 2, 3))
把注意力放在第三个定义函数上,f就是一个函数作为参数,而其在return后作为函数返回。那么现在print一个argument_demo(square_sum, 3, 4)
,相当于return一个square_sum(3, 4)
,也就是square_sum
函数的返回值了。这种写法可以把相似的函数统一成一个大函数使用,仔细想想有点自上而下的意思。我们先把一些过程封装成函数,再把函数封装成大函数,甚至还可以一直封装下去,于是乎我们可以理想的认为一个过程甚至多个相似过程,用一个函数就可以完成啦。
这种方法在面向对象里和回调函数极其相似,就是在触发某个事件时会返回一个方法(这里就是函数了)。我们可以用Python验证一下,这是一个图形编程的例子:
import tkinter as tk
def callback():
listbox.insert(tk.END, 'hello world!')
if __name__ =='__main__':
master = tk.Tk()
button = tk.Button(master, text='ok', command=callback)
button.pack()
listbox = tk.Listbox(master)
listbox.pack()
tk.mainloop()
当我们点击ok按钮时,就会返回一个函数(callback
),打印输出hello world
。
函数里面还可以嵌套函数,并充当返回值。这就好比面向对象中的类,类可以再嵌套类。把函数看做对象,那么函数也可以嵌套了。举个例子:
def line_conf():
b =15
def line(x):
return 2*x+b
b =5
return line
my_line = line_conf()
print(my_line(5))
print(my_line.__closure__)
print(my_line.__closure__[0].cell_contents)
这个例子有点复杂,还用到了几个陌生的东西,下面详细解释一下。我们可以看到,函数内不仅嵌套了函数,而且还引用了变量,但这个变量又和外界没有关系。我们把这种情况称为闭包。在闭包内的变量和函数不能直接在外界使用,只能作为返回值赋值后才能使用。所以有这样一句my_line = line_conf()
,那么my_line
便有了line(x)
函数的功能。作为闭包对象,它拥有很多属性,__closure__
代表一个元组,每个元素都是cell
类型对象,存储着返回对象的值。索引0代表第一个返回的值,也就是返回函数line(x)
的值。而line
和带入的参数b
有关,b
是多少呢?函数的闭包运算全部结束后的值就是该变量的值。这里以return
语句为结束,所以b
取5,所以返回值为15。
闭包函数可以将函数简化,就像刚才把函数作为参数和返回值差不多。举个例子:
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
line11 = line_conf(1, 1)
line22 = line_conf(4, 5)
line33 = line_conf(5, 10)
line44 = line_conf(-2, -6)
print(line1(1), ' ', line2(1), ' ', line3(1), ' ', line4(1))
print(line11(1), ' ', line22(1), ' ', line33(1), ' ', line44(1))
我们定义了5个函数,而前四个可以直接用第5个代替,而且第5个函数还有更多的扩展性,可以实现不用种类的一次函数。而他们因为意义相同,所以两个print
输出的结果是一样的,可见确实减少了代码量。其实这种写法可以看做变相的封装,将4个极其相似的函数封装成一个函数,和把函数作为参数和返回值的应用思想差不多。函数式编程的自上而下思想就是依赖这种方式实现。闭包减少参数也是类似的,看一个例子:
def curve_closure(a, b, c):
def curve(x):
return a*2 + bx + c
return curve
curve1 = curve_closure(1, 2, 1)
print(curve1(1))
将return
的函数给curve1
后,其参数变为1个,减少了。所以说,遇到相似的函数,可以通过大函数预设某些值,就可以有效复用函数,减小参数数量。
3 函数可以升级
我们经常遇到这样的情况,在写完一些函数或类之后,突然发现它应该还要有其他功能。更悲催的是有相当多数量的都需要做类似的整改,或者我希望许多函数或类,都添加相似的功能。这种情况,我们不必费劲心思的逐个添加,而是直接写一个函数为他们量身定制一款装备。这样不仅让思路更清晰,还可以减少代码量。这种装备我们成为装饰器,拥有装饰器后就可以利用道具‘@’将它们升级了。来看个例子:
def decorator_demo(old_function):
def new_function(a, b):
print('input: ', a, b)
return old_function
return new_function
@decorator_demo
def square_sum(a, b):
return a2+b2
@decorator_demo
def square_diff(a, b):
return a2-b2
if __name__ =='__main__':
print(square_sum(3, 4))
print(square_diff(3, 4))
这个例子还是很形象的。我们创造了一个修饰器样板(decorator_demo
),它可以把old_function
转化成new_function
,使得它拥有打印参数这样一个通用的的功能。在定义一个函数之前,我们利用@
符加上修饰器名称,就可以让函数拥有打印参数的功能。我们可以把修饰器看做一个函数,本质上就是函数的嵌套,只是这里换了一种写法,所以它等价于嵌套函数的写法:
square_sum = decorator_demo(square_sum)
既然其本质为函数,那么自然可以携带参数。修饰器的参数写法和函数一样,仅需在名称后加入括号并写入参数即可。举个例子:
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
@pre_str('^_^')
def cubic_sum(a, b):
return a3 + b2
if __name__ =='__main__':
print(cubic_sum(3, 4))
我们在修饰器外面再嵌套一层函数,使得其可以携带参数。其还可以给修饰器添加更多功能,没什么难的,嵌套就是了。利用@pre_str('^_^')
的修饰器携带参数,就可以赋予函数这样的功能。如果改成函数的写法,等价于:
cubic_sum = pre_str('^_^')(cubic_sum)
Python中函数的本质上是对象,所以说类也可以有装饰器。再举个例子:
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
class Bird:
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()
这里就不细讲了,思路都是一样的。它的功能是可以记录调用display
的次数。
这些修饰器最重要的特征是使用@
,利用上一章的知识看,其实无非将一个函数或类引用到一个修饰器上。从这一角度看,修饰器的本质是名称的绑定,通过增加对其引用,使得函数或类有相应的功能。
4 高阶函数
能够将其他函数作为参数的函数称为高阶函数,装饰器本质上就是高阶函数。Python有不少内置的高阶函数供使用,这里将详细讲解。在将高阶函数前,我们先说说lambda
函数。lambda是Python提供的简便的函数定义方式,我们可以用它定义匿名函数。举个例子:
lambda_sum =lambda x, y: x + y
print(lambda_sum(3, 4))
可以看到,lambda
后跟参数,加冒号后跟定义的语句,变量就是函数名,其等价于定义的函数内只有return一条语句:
def lambda_sum(x, y):
return x+y
利用它,我们可以方便的在高阶函数内直接定义函数。
我们讲的第一个高阶函数是Python内置的map(function, list...)
函数,第一个参数代表函数,后面的参数为列表,列表数量以function参数数量确定。举个例子:
data_list = [1, 3, 5, 6]
result =map(lambda x: x +3, data_list)
for item in result:
print(item, end=' ')
map
内函数只有一个参数,所以使用一个列表。map函数会将列表依次和function内的参数对应并带入计算,最终返回一个利用列表依次算出结果的迭代器。使用迭代器可以大幅减少计算机资源的使用,是稳赚不亏的。Python在3版本以后,无论range
函数还是高阶函数,均默认返回迭代器,所以可以用for
循环依次输出。我们再来看一个多参数的例子:
def square_sum(a, b):
return a2+b2
data_list1 = [1, 3, 5, 7]
data_list2 = [2, 4, 6, 8]
result =map(square_sum, data_list1, data_list2)
for item in result:
print(item, end=' ')
程序会把data_list1
带入到a
,把data_list2
代入到b
,并依次计算。从这个例子看,map
可以一次性遍历多个列表,而且仅用一个函数即可,和for
循环功能极其类似但又比它简洁。在并行运算中,我们可以利用map
函数将许多步骤融合为一个map
解决,大大提高了计算机运行效率。
第二个高阶函数是filter
,它是Python的内置函数。filter
内的参数和map
一样,不同的是返回结果。filter英文意为过滤,它返回结果为True的计算结果。举个例子:
def larger100(a):
if a >100:
return True
else:
return False
for item in filter(larger100, [10, 56, 101, 500]):
print(item, end=' ')
这个函数时筛选大于100的数字,所以最后的输出应该是101、500。
第三个函数是reduce
,它来自functools
库,我们可以用import
导入模块。reduce
函数同样接收函数和列表,但它要求作为参数的函数必须有两个参数,列表的数量不一定和参数数量一样。它可以返回一个将结果累计计算后的最终结果。举个例子:
from functools import reduce
data_list = [1, 2, 5, 7, 9]
result = reduce(lambda x, y: x + y, data_list)
print(result)
这次迭代计算的函数时求和。reduce
函数首先从列表取出1
和2
,并求和计算得到3
,接着以3
为函数的一个参数,取下一个序列元素5
作为另一个参数,求和得到8
,以此类推迭代计算,最终得到24
。所以说从这个例子我们可以看到,reduce
函数正如其英文,可以减少数字,将多个数字最终计算成一个数字。它和map
函数一样,可以移植到并行运算中。
我们从文章开头就说明了函数式编程在多线程的重要作用,接下来我们利用学过的高阶函数,包括修饰器,写一个下载资源的代码。我们分别用map
的串行和并行两种形式写出并计算耗费时间。
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*'):
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):
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))
在这段代码中,我们用到了几个陌生的库,multiprocessing
库用于多线程编程,requests
用于网络请求。首先写了一个修饰器decorator_timer
,可以计算函数的运行时间,然后写了一个单线程函数single_thread
和一个多线程函数multiple_thread
,我们让其计算100次。最终结果是惊人的,单线程用了近56秒,而多线程仅用了7秒!Python本就因性能问题饱受诟病,但多线程可以有效的缓和这个问题。
5 函数间的高速通道
函数式编程的核心就是利用纯粹的函数和自上而下的思维模式,利用编程解决问题。在利用函数计算过程中,计算机计算量和资源占用量最大的,无非函数的计算过程以及函数返回结果的存储。计算机计算可以通过并行提高性能,而结果的存储可以利用迭代器提高存储效率。这一节,我们着重讲解如何利用迭代器在函数间传值。
回顾一下之前写过的自定义迭代器:
def gen():
for i in range(4):
yield i
其实它有一个简便的写法:
gen = (x for x in range(4))
同样的如果想生成一个列表,原本应当这么写:
l = []
for x in range(10):
l.append(x**2)
现在它也有简便的写法,只需要把小括号改成中括号即可:
l = [x2 for** x in range(10)]
字典的生成也有简便的写法:
d = {k: v for k,v in enumerate('Vamei') if val not in 'Vi'}
不过这里是建议使用迭代器的,它可以占用更少的内存。这种利用迭代器的方式在函数式编程中成为懒惰求值。所以如果我们可以把函数的返回值均变为迭代器,那么计算机的存储效率就能大大提高。Python为我们提供一个itertools
库,里面提供了大量的直接生成的迭代器提供我们使用。
首先我们将函数库导入:from itertools import *
库中有许多迭代器,我们先来了解几个无限重复的。count
函数可以接受两个参数,第一个是初始数字,第二个是迭代量。cycle
函数接收一个参数,可以将该参数的每个元素循环迭代输出。repeat
函数在一个参数时可以重复输出该对象。举几个例子:
count(5, 2) # 从5开始输出迭代,每次加2。如5 7 9......
cycle('abc') # 一直重复a b c a b......
repeat(1.2) # 重复输出1.2,1.2 1.2 1.2......
其实repeat
在接收两个参数时,第二个参数表示重复次数,如:
repeat(10, 5) # 重复输出5次10
库中还有用于组合的迭代器。chain
函数可以拼接迭代器,product
函数可以将接收的迭代器做笛卡尔积,相当于嵌套循环。举几个例子:
chain([1, 2, 3], [4, 5, 7]) # 连接连个列表转变为迭代器,1 2 3 4 5 7
product('abc', [1, 2]) # 将这两个序列做笛卡尔积
此外还有其他的组合函数。permutations
接收两个参数,第一个是对象,第二个是挑选的元素个数。它可以挑选对象的一定数量的元素组合,并按根据对象内元素排序,和product
极其类似。combinations
函数接收同样的参数,但组个时不区分顺序,若组合相同的元素则仅返回一个。combinations_with_repalcement
函数也接收两个同样的参数,但组合的元素可以是重复的。举几个例子:
permutations('abc', 2) # 输出 ab ac ad ba ......
combinations('abc', 2) # 输出 ab ac ad bc bd ......
combinations_with_replacement('abc', 2) # 输出 aa ab ac ad ba bb ......
库中还有许多高阶函数。starmap
接收两个参数,第一个是函数,第二个是元组序列,它可以将序列中的每个元组依次给函数计算结果,并返回迭代器。takewhile
函数接收两个参数,第一个是函数,第二个是对象,当函数返回True时则收集该元素,只要返回Flase立刻停止收集,将最后结果返回为迭代器。dropwhile
函数参数和takewhile
一样,但是其功能不同,当函数返回Flase时跳过该元素,一旦返回True,则开始接收剩余的所有元素,并返回一个迭代器。举几个例子:
starmap(pow, [(1, 1), (2, 2), (3, 3)] # 返回 1 4 27
takewhile(lambda x: x<5, [1, 3, 6, 7, 1]) # 返回 1 3
dropwhile(lambda x: x<5, [1, 3, 6, 7, 1]) # 返回 6 7 1
库中还提供了针对字典的迭代器。groupby
函数接收两个参数,第一个是对象,一般利用sorted
排序,第二个是分类标准函数。先举个例子:
from itertools import *
def height_class(h):
if h >180:
return 'tall'
elif h <160:
return 'short'
else:
return 'middle'
friends = [191, 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, ' *:* ', list(n))
这个函数height_class
用于身高分类,不同身高return不同结果。我们可以利用sorted
函数,以该函数return的结果为键,满足要求的数据集为值进行排序。然后我们利用groupby
,利用该函数为键。因为已经用sorted函数
排好序,所以可以直接分类。groupby
将返回一个字典迭代器,键为分类的返回值,值为分类的结果,我们可以将它变为序列后输出,输出结果为:
middle: [165, 170, 177]
short: [158, 159]
tall: [191, 181, 182, 190]
库中还有一些其他的函数供迭代使用。compress
函数接收两个参数,第一个是对象,第二个是列表,它会根据列表内的真假情况保留元素,为True就保留。islice
函数和slice函数类似,可以将对象切片,但它返回的是迭代器。izip
和zip
类似,可以分别将列表相同位置的元素变为元组,并放在一个序列中,但它返回一个迭代器。
库中还有许多许多函数,这里不多做介绍了,可以自行学习。我们学习函数式编程,就是为了学习其编程思想和理念。不光是Python,其他的编程语言也可以用到的,这是一个编程思维的顶层建筑,可以对任意的编程问题有效。总的来说本书的知识基本总结完毕,最后,让我们导入this
库:import this
,并运行,看看这首Python之道的诗为结束吧。
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than right now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
实例代码请看我的码云:第七章样例代码