第七章 函数式编程

7.1 又见函数

1.Python中的函数式

前面介绍了面向进程和面向对象的编程,这一章讲解面向函数的编程即函数式编程。函数式编程的本质在于封装,它以函数为中心进行代码封装,函数在面向进程中涉及过,它有参数和返回值,起到输入和输出数据的功能。
函数式编程强调了函数的纯粹性,一个纯函数是没有副作用的,即一个函数不会影响其他函数。我们在之前的可变对象中演示过,在函数内部改变列表里面的对象,会影响函数外部的列表元素,其他调用该列表的函数也会产生相应的影响,这就产生了副作用。所以,为了是函数纯粹,我们在函数内部使用的都是不可变更的变量。

由于Python中存在可变对象,因此只能尽量避免副作用。
函数式编程由于函数相互独立,因此不用担心函数调用对其他函数的影响,使用起来更加简单。
而且,纯函数也方便进行并行化运算

在进行并行化编程时,我们经常担心不同进程之间相互干扰的问题。当多个进程或者线程同时修改一个变量时,进程或线程的先后顺序会影响最终结果,比如

from threading import Thread 

x = 5

def double():
    global x  # 声明全局变量x
    x = x * 2
    
def plus_ten():
    global x  # 声明全局变量x
    x = x + 10
    
th1 = Thread(target = double)  # 线程1
th2 = Thread(target = plus_ten) # 线程2

th1.start()  # 线程1开始执行
th2.start()  # 线程2开始执行

th1.join()   # 线程1结束
th2.join()   # 线程2结束 

print(x)

结果:
20

将上面的线程调换一下
th2.start()  # 线程2开始执行
th1.start()  # 线程1开始执行

th2.join()   # 线程2结束 
th1.join()   # 线程1结束

print(x)

结果:
30

global是声明全局变量的关键字,函数对全局变量的修改能被其他函数看见,因此造成了副作用。如果并行地执行上面两个函数,执行顺序是不确定的,结果也不一样。比如先执行double,再执行plus_ten,得到的结果是20;若先执行plus_ten,再执行double,得到的结果是30。这被称为竟跑条件,是并行编程中需要尽力避免的。

而函数式编程消灭了副作用,也在无形之中消除了竟跑条件的可能。因此,函数式编程天然地适用于并行化运算。
Python中加入了lambda函数、map、filter、reduce等高阶函数,引入了函数式编程的特征。

函数式编程的思路是自上而下的,先提出一个大问题,在最高层定义一个函数来解决这个大问题,在函数的内部,再用其他函数来解决小问题。比如,我们要解决"如何把大象放进冰箱"的问题,首先我们定义一个函数"把大象放进冰箱"来解决大问题,然后在"把大象放进冰箱"里面,有三步函数"打开冰箱门"、"放入大象"、"关上冰箱",如果需要继续细化,则在这三步函数内部继续调用其他函数。

2.并行运算

并行运算是指多条指令同时执行。对应的串行运算指的是同一时间只能执行一条指令。

大规模的并行运算一般是在多个主机组成的集群上进行的,主机之间通过高速的网络设备通信。但由于集群的成本过高,我们可以在单机上通过多线程或多进程来模拟集群的并行处理。

在一台单机上,往往运行着多个程序,即进程。如用浏览器上网时,用网易云听音乐,计算机就同时运行两个进程。这就是单机"分时复用"的方式,把运算能力分给多个进程。
也就是说,集群和单机都实现了多个进程的并行运算。集群上的多进程分布在不同的主机,而单机则使用"分时复用"的方式来实现。下面看一个多进程的例子

import multiprocessing as mulpr

def proc1():
    return 333*333

def proc2():
    return 444*444

p1 = mulpr.Process(target = proc1())
p2 = mulpr.Process(target = proc2())

p1.start()  # 启动进程
p2.start()

p1.join()   # 结束进程
p2.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 argF_demo(f,a,b): #  f是函数,a是数字,b是数字
    return f(a,b)

print(argF_demo(square_sum,3,5))
print(argF_demo(cubic_sum,3,5))

结果:
34
152

argF_demo()的第一个参数f是一个函数对象。square_sum和cubic_sum作为f传递给argF_demo()。

再如,图形用户界面中,作为参数的函数经常起到回调(callback)的作用。当某个事件发生时(如界面中的某个按钮被点击),回调函数就会被调用。下面是GUI回调的例子

import tkinter as tk

def callback():
    listbox.insert(tk.END,"我出现了,我是回调函数")
    
if __name__ == "__main__":
    master = tk.Tk()
    
    button = tk.Button(master,text = "OK",command = callback) 
    # 创建一个按钮,一旦点击就会使用callback函数
    button.pack()
    
    listbox = tk.Listbox(master)
    listbox.pack()
    
tk.mainloop()

Python中内置了tkinter的图形化功能。listbox是列表栏,就是上图的显示框,一旦点击按钮OK,就会在列表栏中插入"我出现了,我是回调函数 "。

2.函数作为返回值

函数是一个对象,除了可以作为函数的参数,还可以作为函数的返回值。

def line_res():
    def line(x):
        return 2*x + 1
    return line  # 返回一个函数

line1 = line_res()
print(line1(3))

结果:
7

line_res将函数line作为对象返回给line1,line1通过输入参数就可以得到结果11。
从上例可以看出,函数里面再定义了函数,函数内部的函数对象也有作用域,Python中用缩进块来表示。如

def line_res():
    def line(x):
        return 2*x + 1
    print(line(3))  # 作用域内

line_res()  # 打印出7
print(line(3)) # 作用域之外,将会报错

结果:
7
NameError: name 'line' is not defined

line()函数在line_res()的作用域之内,只能在line_res()之内调用,当在line_res()的作用域之外调用line()函数时,将会报错。

3.闭包

闭包:一个函数与它的环境变量合在一起,即为闭包。
用一个例子来理解闭包和环境变量

def line_res():
    b = 15  # 这个就是line()的环境变量
    def line(x):
        return 2*x + b
    b = 5   # line()的环境变量
    return line  # 返回函数对象

line1 = line_res() # 将函数对象赋给line1
print(line1(3))

结果:
11

line()函数的引用了它作用域外的变量b,b是line()外部的变量,这个b就是line()函数的环境变量。line()函数和环境变量b合在一块,这就是闭包。
上面代码中,b分别在line()定义前后有两次不同的值,最终结果是11,即line()中的b=5。因此,闭包中的环境变量是内部函数作为对象返回时,最近赋值的那个值。


在Python中,闭包(closure)是一个包含环境变量取值的函数对象。环境变量取值被复制到函数对象的__closure__属性中。

def line_res():
    b = 15  # 这个就是line()的环境变量
    def line(x):
        return 2*x + b + a
    
    b = 5   # line()的环境变量
    a = 2   # line()的环境变量
    
    return line  # 返回函数对象

line1 = line_res()
print(line1.__closure__)  # 闭包
print(line1.__closure__[0].cell_contents) # 打印2 也即是a=2
print(line1.__closure__[1].cell_contents) # 打印5 也即是b=5

结果:
(<cell at 0x000001CC6DA83288: int object at 0x00007FFBB9B19360>,
 <cell at 0x000001CC6DDFFFD8: int object at 0x00007FFBB9B193C0>)
2
5

可以看出,闭包属性中包含了一个元组,元组中是cell型的对象。
第一个是2,即环境变量a=2,第二个是5,即b=5。


闭包可以提高代码的复用性。

def line1():
    return x + 1

def line2():
    return 2*x + 1

def line3():
    return 4*x + 2

上面一共有三条直线,可以看出都有一个自变量x,还有另外两个数,可以定义为a和b。则上面的直线可以统一写为:a*x+b。用闭包来改写代码

def line_res(a,b):
    def line(x):
        return a*x + b
    return line

line1 = line_res(1,1)
line2 = line_res(2,1)
line3 = line_res(4,2)
print(line1(2),line2(2),line3(2))

结果:
3 5 10

闭包还可以起到减少函数参数的作用。

def curve(a,b,c,x):  # 定义一个二次函数,需要4个参数a,b,c,x
    return a*(x**2) + b*x + c

print(curve(2,4,3,1))
print(curve(2,4,3,2))
结果:
9
19

可见每次计算一个二次函数值需要输入很多参数,尽管我们只是自变量x改变而其他参数不变。我们可以通过闭包来减少参数

def curve_closure(a,b,c):    
    def curve(x):
        return a*(x**2) + b*x + c
    return curve

curve1 = curve_closure(2,4,3)
print(curve1(1))
print(curve1(2))

结果:
9
19

这段代码可以与上面的代码比较,需要输入的参数少了3个。

7.3 小女子的梳妆匣子

1.装饰器

装饰器是一种高级Python语法。它可以对一个函数、方法或类进行加工。
首先我们定义两个函数,一个计算平方和,一个计算平方差。

def square_sum(a,b):
    return a**2 + b**2

def square_diff(a,b):
    return a**2 - b**2

print(square_sum(4,3))
print(square_diff(4,2))

结果:
25
12

如果我们想要显示我们的输入,可以改进

def square_sum(a,b):
    print("输入的数据",a,b)
    return a**2 + b**2

def square_diff(a,b):
    print("输入的数据",a,b)
    return a**2 - b**2

print(square_sum(4,3))
print(square_diff(4,2))

结果:
输入的数据 4 3
25
输入的数据 4 2
12

可以看出来,打印输入数据的操作语句一样,我们可以将这个打印输入数据的操作写成装饰器,然后用于函数上。

def decorator_demo(old_fun): # 定义一个装饰器
    def new_fun(a,b):
        print("输入的数据",a,b)
        return old_fun(a,b)
    return new_fun

@decorator_demo
def square_sum(a,b):
    return a**2 + b**2

@decorator_demo
def square_diff(a,b):
    return a**2 - b**2

print(square_sum(4,3))
print(square_diff(4,2))

结果:
输入的数据 4 3
25
输入的数据 4 2
12

装饰器用def定义,它接收了一个可调用对象(如函数对象)作为输入参数,并且返回一个新的可调用对象,如上面的new_fun()。在new_fun()中,我们增加了打印的功能,同时也保留了原来old_fun的功能。

定义好装饰器之后,我们通过"@"语法使用它。
在函数square_sum(),square_diff()之前调用装饰器@decorator_demo,实际上就是将原来的函数square_sum(),square_diff()传递给decorator_demo(),然后将decorator_demo()返回的新函数对象赋给原来的函数名square_sum(),square_diff()。即如在调用square_sum(3,4)时,实际发生

装饰器起到的作用就是,让同一个变量名指向一个新函数对象,从而达到修改函数对象的目标。而且由于原来函数已经定义的差不多了,只需要稍加修饰即可,仍然保留原函数的功能,这就是修饰器的作用。

我们可以再来举一个例子,假设我们想知道函数的运行时间。这个对于每个函数都是适用的,因此我们可以把这个功能做成装饰器。

import time as t
def decorator(old_fun):
    def new_fun(*arg,**dict_arg):
        start = t.perf_counter()
        res = old_fun(*arg,**dict_arg)
        end = t.perf_counter()
        print("函数运行时间为:",end - start)
        return res
    return new_fun

@decorator
def curve(a,b,c,x):
    return a*x**2 + b*x + c

print(curve(2,3,4,1))

结果:
函数运行时间为: 4.700001227320172e-06
9

2.带参装饰器

装饰器允许我们使用它时,传入其他参数。

def pre_str(pre): # 带参的装饰器
    def decorator(old_fun):
        def new_fun(a,b):
            print(pre,"输入的数据是",a,b)
            return old_fun(a,b)
        return new_fun
    return decorator

#装饰square_sum()
@pre_str(">_<")
def square_sum(a,b):
    return a**2 + b**2

#装饰square_dif()
@pre_str("^_^")
def square_dif(a,b):
    return a**2 - b**2

print(square_sum(3,4))
print(square_dif(2,1))

结果:
>_< 输入的数据是 3 4
25
^_^ 输入的数据是 2 1
3

pre_str是一个带参装饰器,对原来装饰器进行封装,返回的是一个装饰器。当我们使用@pre_str("_")时,Python能够发现这一层的封装,并将参数传入到装饰器中,相当于


根据参数的不同,带参装饰器会对函数进行不同的加工,进一步提高装饰器的适用范围。

3.装饰类

装饰器还可以装饰类,一个装饰器装饰一个旧类,并返回一个新类,起到了加工类的效果。

def decorator_Class(OldClass):
    class NewClass(object):
        def __init__(self,age):
            self.total_display = 0
            self.wrapped = OldClass(age) # 将旧类的对象进行打包
        
        def display(self):
            self.total_display += 1
            print("展示次数",self.total_display)
            self.wrapped.display()  # 调用旧类对象的功能或数学
    return NewClass # 最终返回一个新类

@decorator_Class
class Bird(object):
    def __init__(self,age):
        self.age = age
    
    def display(self):
        print("我的年龄是:",self.age,"岁")
        
eagle = Bird(5)
for i in range(3):
    eagle.display()

在装饰器decorator_class中,我们定义了新类,在新类中的初始化方法中,用self.wrapped将旧类的对象进行打包,并附加了新属性total_display,用于记录展示次数。而且我们还同时更改了display方法。通过装饰,旧类就可以显示调用display()的次数。

7.4 高阶函数

1.lambda与map函数

能接收其他函数作为参数的函数,称为"高阶函数"。像上面介绍的装饰器,也属于高阶函数。最具有代表性的高阶函数有:map()、filter()、reduce()。


在讲解高阶函数之前,先讲解匿名函数lambda。lambda语法也可以用来定义函数,只不过比较实用简短语句的函数。如

lambda_sum = lambda x,y:x+y
print(lambda_sum(3,4))

通过lambda,我们创建了一个匿名函数对象,它的参数是x,y,返回值是x+y,然后将它赋值给lambda_sum()。lambda_sum()的使用与正常函数一样。lambda定义的函数适用于简短函数。


高阶函数从map()开始介绍,map()的第一个参数是函数对象,它把这一个函数对象作用于后面多个元素。第二个元素之后都是循环对象。

data1 = [1,3,5,7]
res = map(lambda x:x+3,data1) # x : 4 7 8 10

map()的第一个参数是函数对象,第二个参数是可循环对象。对于data1中的每个元素,都会成为lambda函数的参数,lambda函数都会调用一次。也就是说,map()接收到的函数对象参数依次作用于每一个参数。map()会返回一个迭代器。上面的代码就相当于如下

def generator(fun,iterator):
    for item in iterator:
        yield fun(item)
        
data1 = [1,3,5,7]
res = generator(lambda x:x+3,data1)

lambda函数也可以是多个参数的,如

def square_sum(x,y):
    return x**2 + y**2

data1 = [1,3,5,7]
data2 = [2,4,6,8]

res = map(square_sum,data1,data2)

map()函数中第一个参数是函数对象,第二和第三个是可循环对象。第二个循环对象对应于square_sum中的x,第三个循环对象对应于square_sum中的y。

2.filter函数

filter函数与map函数相似,但filter()只有两个参数,第一个参数也是函数对象,第二个参数都是循环对象。如果函数对象返回的是True,则该元素被放到迭代器中,即filter()函数通过调用函数来筛选数据。如

def larage100(x):
    if x > 100:
        return True
    else:
        return False

for item in filter(larage100,[1,101,202,5]):
    print(item)

结果:
101
202

import math
def is_Sqr(x):
    return math.sqrt(x) % 1 == 0  # 判断平方根是否为整数
 
newlist = filter(is_Sqr, range(1, 101))
for item in newlist:
    print(item,end = " ")

结果:
1 4 9 16 25 36 49 64 81 100

filter()函数顾名思义,就是一个过滤器,更多的用于筛选出符合我们定义的函数对象里面的条件的数据。

3.reduce函数

reduce函数一共有三个参数,第一个是函数对象,且这个函数对象能够接收两个参数;第二个参数是序列;第三个参数是初始值,无初始值就按照序列的第一个为初始值。

from functools import reduce

res1 = reduce(lambda x,y:x+y,[1,3,5,7,9])
print(res1)

res2 = reduce(lambda x,y:x+y,["x","y","z"],"a")
print(res2)

结果:
24
axyz

从第一个res1看出,它接收两个参数x和y,将列表中的元素进行累进运算,即(((1+3)+5)+7)=16;相似的res2也是如此。


reduce通过二元运算,将多个元素聚集成一个结果。map和reduce都是单线程的,运行效果和循环类似,但map和reduce可以方便的移植入并行化的运行环境下。
在并行运算中,reduce紧接着map运算,map将运算结果分布在多个主机上,reduce运算把结果收集起来。谷歌用于并行运算的软件架构叫MapReduce

4.并行处理

import time
from multiprocessing import Pool
import requests

# 定义一个运行时间的修饰器
def decorator_timer(oldFun):
    def newFun(*arg,**dictarg):
        start = time.perf_counter()
        res = oldFun(*arg,**dictarg)
        end = time.perf_counter()
        print("运行时间为:",end - start)
        return res
    return newFun

# 访问网页任务
def download_once(i,addres="http://cnblogs.com"):
    print("第",i+1,"次访问完成")
    r = requests.get(addres)
    return r.status_code

# 单线程,一次处理一个
@decorator_timer
def single_thread(f,times):
    print("我是单线程,每次只执行一次")
    res = map(f,range(times))
    return list(res)

# 多线程,并行处理
@decorator_timer
def multiple_thread(f,times,process_num = 5):
    print("我是多线程,任务同时完成")
    p = Pool(process_num)
    res = p.map(f,range(times))
    return list(res)

if __name__ == "__main__":
    TOTAL = 10
    print(single_thread(download_once,TOTAL))
    print()
    print(multiple_thread(download_once,TOTAL))

结果:
我是单线程,每次只执行一次
第 1 次访问完成
第 2 次访问完成
第 3 次访问完成
第 4 次访问完成
第 5 次访问完成
第 6 次访问完成
第 7 次访问完成
第 8 次访问完成
第 9 次访问完成
第 10 次访问完成
运行时间为: 12.258385399999952
[200, 200, 200, 200, 200, 200, 200, 200, 200, 200]

我是多线程,任务同时完成
运行时间为: 2.3248039000000063
[200, 200, 200, 200, 200, 200, 200, 200, 200, 200]

上面的download_once是单次的访问网页任务,TOTAL是访问次数需求,一共是10次。
单线程一次一次的进行,最终获取了10个状态码;
而多线程开辟了5个进程,10次任务同时进行,把10个任务分配给了5个工人。从运行时间可以明显看出,多线程的运算更加快速。

7.5 自上而下

1.便捷式表达

Python中有几种体现自上而下思维的语法,如生成器表达式、列表解析与词典解析。

  • 生成器表达式
    生成器表达式是构建生成器的便捷表达式。假设我们要构建一个生成器
def gen():
    for i in range(4):
        yield i

上面代码用生成器表达式可以改写为

gen = (x for x in range(4))
  • 列表解析(列表生成式)
    若我们想要一个0-9每个数的平方的列表,可以这样生成
li = []

for x in range(10):
    li.append(x**2)
    
print(li)

结果:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

更加便捷地,我们可以使用列表解析,或者说列表生成式

li = [x**2 for x in range(10)]
print(li)

结果:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

列表生成式的语法很直观,首先说明我们需要的是什么样的元素,再通过for加入限定条件,即哪些元素。再如生成3个随机数列表,每个列表包含10个1到100内的随机整数

import random as r

ran_li1 = [r.randint(1,100) for i in range(10)]
ran_li2 = [r.randint(1,100) for i in range(10)]
ran_li3 = [r.randint(1,100) for i in range(10)]

print(ran_li1)
print(ran_li2)
print(ran_li3)

结果:
[47, 10, 21, 26, 37, 62, 28, 57, 40, 77]
[3, 7, 56, 70, 9, 17, 1, 60, 46, 96]
[59, 98, 87, 79, 55, 52, 37, 31, 84, 39]

我们还可以在列表解析里面加入if语法

x1 = [1,3,5,7]
y1 = [2,4,6,8]

li = [x**2 for x,y in zip(x1,y1) if y > 5] 
# 当y>5时,生成对应的x的平方,再保存入列表

print(li)

结果:
[25, 49]

zip()是一个将序列打包的方法,而for i in zip()则可以看成得到序列中的每个元素,即解压。

a = [1,2,3]
b = [4,5,6]
c = [4,5,6,7,8]

for i in zip(a,b,c):
    print(i)

结果:
(1, 4, 4)
(2, 5, 5)
(3, 6, 6)

--------------------------------------------------

x1 = [1,3,5,7]
y1 = [2,4,6,8,10]
z1 = ["a","b","c","d","e"]

for x,y,z in zip(x1,y1,z1):
    print(x,y,z)

结果:
1 2 a
3 4 b
5 6 c
7 8 d
  • 词典解析(词典生成式)
    类似的,词典也可以快捷生成。与列表解析的语法类似。
d1 = {key:val for key,val in enumerate("HelloWorld")}
print(d1)

d2 = {k:v for k,v in enumerate("Python") if v in "Py"}
print(d2)
"""
enumerate是枚举,包括索引和元素
"""
结果:
{0: 'H', 1: 'e', 2: 'l', 3: 'l', 4: 'o', 5: 'W', 6: 'o', 7: 'r', 8: 'l', 9: 'd'}
{0: 'P', 1: 'y'}

2.懒惰求值

迭代器有时候看起来像列表,但实际上迭代器的元素是实时运算出来的,在使用元素之前,是不会占据空间;而列表在建立时,就先产生了元素,并保存在空间内。
迭代器的工作方式就是懒惰求值,即需要的时候再使用元素,才会计算具体的值。

import time
start1 = time.perf_counter()
for i in (x**2 for x in range(100)):
    print(i)
end1 = time.perf_counter()
print("迭代器运行时间:",end1-start1)

结果:
0
1
4
9
……
迭代器运行时间: 0.004747199999656004
import time
start2 = time.perf_counter()
for i in [x**2 for x in range(100)]:
    print(i)
end2 = time.perf_counter()
print("列表运行时间:",end2 - start2)

结果:
0
1
4
9
……
列表运行时间: 0.008195000000341679

通过上面的对比可以看出,虽然生成的结果相同,但是迭代器的运行时间小于使用列表运行的时间。这是因为建立列表需要先计算产生元素,再保存生成入列表,再使用列表;而迭代器却是"懒惰求值"

map和range函数返回的都是迭代器,它们所做的也是懒惰求值,需要的时候再使用;但是若将它们转化为列表,时间将会大大增加、如

#未转化为列表之前
import time as t
start = t.perf_counter()
ite = range(100)
res = map(lambda x:x**2,ite)
print(res) # 返回的是一个迭代器
end = t.perf_counter()
print("运行时间为:",end-start)

结果:
<map object at 0x0000018149C5B8D0>
运行时间为: 0.00012900000001536682

转化为列表

import time as t
start = t.perf_counter()
ite = range(1000)
res = map(lambda x:x**2,ite)
print(list(res))
end = t.perf_counter()
print("运行时间为:",end-start)

结果:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401, 2500, 2601, 2704, 2809, 2916, 3025, 3136, 3249, 3364, 3481, 3600, 3721, 3844, 3969, 4096, 4225, 4356, 4489, 4624, 4761, 4900, 5041, 5184, 5329, 5476, 5625, 5776, 5929, 6084, 6241, 6400, 6561, 6724, 6889, 7056, 7225, 7396, 7569, 7744, 7921, 8100, 8281, 8464, 8649, 8836, 9025, 9216, 9409, 9604, 9801]
运行时间为: 0.00015100000018719584

可见100次时,生成为迭代器的时间只需要0.000129,而转化为列表需要0.000151,虽然相差不大,但是已经生成为迭代器的时间已经小于生成为列表的时间。如果将100设置为1000,相差的时间更大。如

1000次时

生成为迭代器的时间
运行时间为: 0.00013020000005781185

生成为列表的时间
运行时间为: 0.0005742000003010617

可以看出,将map生成的结果转化为列表,时间将大大增加。

再者,如果我们不需要穷尽所有数据元素,那么懒惰求值将节省很多时间,而列表生成式的方式,提前准备的数据就会造成极大浪费。如

import time as t
start1 = t.perf_counter()
for i in (x**2 for x in range(100000)):
    if i > 500:
        break
end1 = t.perf_counter()
print("迭代器运行时间:",end1-start1)

start2 = t.perf_counter()
for i in [x**2 for x in range(100000)]:
    if i > 500:
        break
end2 = t.perf_counter()
print("列表运行时间",end2-start2)

结果:
迭代器运行时间: 1.700000029813964e-05
列表运行时间 0.04208400000061374

迭代器的运行时间为1.7*10-5,而列表运行时间是0.0420,可见 迭代器更快。也可以看出,除了运行更快外,懒惰求值还可以节省内存空间。

除了map、filter函数,Python中itertools包提供丰富的操作迭代器的工具。

3.itertools包

首先导入itertools包

from itertools import *
  • count()和cycle()可以生成无限循环的迭代器。

count(5,2):从5开始每次增加2,无限循环

# count(5,2):从5开始每次增加2,无限循环
for i in count(5,2):
    print(i)
    if i > 15:  # 到15结束,不然无限循环了
        break

结果:
5
7
9
11
13
15
17

cycle(list|str|tuple|dict):不断重复序列中的元素

# cycle(list|str|tuple|dict):不断重复序列中的元素
cut1 = 0
for i in cycle([1,3,"ab"]):
    cut1 += 1
    print(i)
    if cut1 == 7:  # 不断循环,直到第7次时跳出
        break

结果:
1
3
ab
1
3
ab
1
  • repeat():返回一个不断重复元素的迭代器,也可以有次数限制的重复。
# repeat():不断重复元素,也可以有次数限制
cut2 = 0
for i in repeat(5):
    cut2 += 1
    print(i)
    if cut2 == 7:# 不断循环,直到第7次时跳出
        break
    
for i in repeat([1,"a",1.2],4): # 重复4次该列表
    print(i)

结果:
5
5
5
5
5
5
5
[1, 'a', 1.2]
[1, 'a', 1.2]
[1, 'a', 1.2]
[1, 'a', 1.2]
  • 组合旧迭代器,生成新迭代器

chain():连接两个及以上的迭代器

# chain():连接两个及以上的迭代器
for i in chain([1,3,5,7],[2,4,6,8]):
    print(i)

结果:
1
3
5
7
2
4
6
8

product():返回多个迭代器的笛卡尔积,即得到元素的所有可能的组合方式,相当于嵌套循环。

# product():返回多个迭代器的笛卡尔积,
# 即得到元素的所有可能的组合方式,相当于嵌套循环
for i in product("abc",[1,3]): # 以元组来接收
    print(i)

for x,y in product("abc",[1,3]):# 以每个元素来接收
    print(x,y)

结果:
('a', 1)
('a', 3)
('b', 1)
('b', 3)
('c', 1)
('c', 3)
a 1
a 3
b 1
b 3
c 1
c 3

permutations("abc",2):从"abc"中挑2个元素,将所有结果排序,返回新的迭代器,且组合区分顺序。

'''
permutations("abc",2)
从"abc"中挑2个元素,将所有结果排序,
返回新的迭代器,且组合区分顺序
'''    
for i in permutations("abc",2):
    print(i)

结果:
('a', 'b')
('a', 'c')
('b', 'a')
('b', 'c')
('c', 'a')
('c', 'b')

combinations("abcd",3):从"abcd"中挑3个元素进行组合,将所有结果排序,返回新迭代器且组合不区分顺序,即ab和ba都是ab

'''
combinations("abcd",3)
从"abcd"中挑3个元素进行组合,
将所有结果排序,返回新迭代器
且组合不区分顺序,即ab和ba都是ab
'''
for i in combinations("abcd",3):
    print(i)

结果:
('a', 'b', 'c')
('a', 'b', 'd')
('a', 'c', 'd')
('b', 'c', 'd')

combinations_with_replacement("abcd",3):与上面类似,但是允许出现重复的元素,如a,a,a。

'''
combinations_with_replacement("abcd",3)
与上面类似,但是允许出现重复的元素,如a,a,a
'''
for i in combinations_with_replacement("abcd",3):
    print(i)

结果:
('a', 'a', 'a')
('a', 'a', 'b')
('a', 'a', 'c')
('a', 'a', 'd')
('a', 'b', 'b')
('a', 'b', 'c')
('a', 'b', 'd')
('a', 'c', 'c')
('a', 'c', 'd')
('a', 'd', 'd')
('b', 'b', 'b')
('b', 'b', 'c')
('b', 'b', 'd')
('b', 'c', 'c')
('b', 'c', 'd')
('b', 'd', 'd')
('c', 'c', 'c')
('c', 'c', 'd')
('c', 'd', 'd')
('d', 'd', 'd')
  • starmap(pow,[[1,1],(2,2),(3,3)]):将pow作用于列表中的每个序列
'''
starmap(pow,[[1,1],(2,2),(3,3)])
将pow作用于列表中的每个序列
'''
for i in starmap(pow,[[1,1],(2,2),(3,3)]):
    print(i)

结果:
1
4
27
  • takewhile(lambda x:x>2,[1,2,5,6]):只有两个参数。当函数返回True时,收集元素到迭代器。一旦返回False,就停止。
'''
takewhile(lambda x:x>2,[1,2,5,6]),只有两个参数
当函数返回True时,收集元素到迭代器。一旦返回False,就停止。
'''
for i in takewhile(lambda x:x<6,[1,2,5,6]):
    print(i)

结果:
1
2
5

dropwhile(lambda x:x<3,[1,2,3,2,1,8]):当函数返回False时,跳过并记录元素。一旦返回True,则开始收集剩下的所有元素到迭代器。

'''
dropwhile(lambda x:x<3,[1,2,3,2,1,8])
当函数返回False时,跳过并记录元素
一旦返回True,则开始收集剩下的所有元素到迭代器。
'''
for i in dropwhile(lambda x:x<3,[1,2,3,2,1,8]):
    print(i)

结果:
3
2
1
8

groupby():能将一个key函数作用于原迭代器的各个元素,从而获得各个函数的键值。根据key()函数的结果,来对原来迭代器的元素进行分组。

'''
groupby():
能将一个key函数作用于原迭代器的各个元素,从而获得各个函数的键值。
根据key()函数的结果,来对原来迭代器的元素进行分组。
'''
def height_keyFun(height):
    if height > 180:
        return "Tall"
    elif height < 160:
        return "Short"
    else:
        return "Middle"

People = [191,160,158,172,185,157,179]
People = sorted(People,key = height_keyFun) # 按照height_keyFun进行排序,
                                           # 让同组元素先在位置上靠拢。
print(People)

for x,y in groupby(People,key = height_keyFun):
    print(x) # x是键
    print(list(y)) # y是一个迭代器,相对应于键的值,转化为列表才可以显示

结果:
[160, 172, 179, 158, 157, 191, 185]
Middle
[160, 172, 179]
Short
[158, 157]
Tall
[191, 185]
  • 方便迭代器构建的工具
    compress(["a",1,9,7],[1,0,0,1]):根据真假情况,选择保留序列中的元素。True保存,False不保存
'''
compress(["a",1,9,7],[1,0,0,1])
根据真假情况,选择保留序列中的元素。True保存,False不保存
'''
for i in compress(["a",1,9,7],[1,0,0,1]):
    print(i)

结果:
a
7

islice()与slice相似,只不过返回的是一个迭代器

'''
slice(start,stop,step) 切片器
start -- 起始位置
stop -- 结束位置
step -- 步长

islice()与slice相似,只不过返回的是一个迭代器
'''
li = [1,2,3,"a","z"]
print("切片slice1",li[slice(1,4,1)])
print("切片slice2",li[slice(3)])
print("我是slice,返回的是函数对象",slice(3))

print("我是islice,返回的是迭代器",islice(li,4))
for i in islice(li,5):
    print(i,end = " ")

结果:
切片slice1 [2, 3, 'a']
切片slice2 [1, 2, 3]
我是slice,返回的是函数对象 slice(None, 3, None)
我是islice,返回的是迭代器 <itertools.islice object at 0x000001814A824958>
1 2 3 a z 

zip_longest()与zip()相似,但是返回的是一个迭代器,且按照的是最长的序列来组合的

'''
zip()在上述中讲过,是按照最短序列来组合的
zip_longest()与其相似,但是返回的是一个迭代器,且按照的是最长的序列来组合的
'''
a = [1,"abc",8,"x","yz"]
b = [9,"ok","Python",777]

print("我是zip(),返回的是一个函数对象,是按照最短序列来组合的",zip(a,b))
for i in zip(a,b):
    print(i)

print()

print("我是zip_longest(),返回的是一个迭代器,\
      且按照的是最长的序列来组合的,没有就给None",zip_longest(a,b))
for i in zip_longest(a,b):
    print(i)

结果:
我是zip(),返回的是一个函数对象,是按照最短序列来组合的
 <zip object at 0x0000018149C7D9C8>
(1, 9)
('abc', 'ok')
(8, 'Python')
('x', 777)

我是zip_longest(),返回的是一个迭代器,且按照的是最长的序列来组合的,没有就给None 
<itertools.zip_longest object at 0x000001814A824958>
(1, 9)
('abc', 'ok')
(8, 'Python')
('x', 777)
('yz', None)

以上是itertools包的各种功能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容