复习时先看看这个:https://blog.csdn.net/weixin_39880623/article/details/110153616?
第一章 函数式编程
一、生成器 (generator)
-
通过列表⽣成式,我们可以直接创建⼀个列表。如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推 算出后续的元素呢?这样就不必创建完整的list,从⽽节省⼤量的空间。在Python中,这种⼀边循环⼀边计算的机 制,称为⽣成器:generator。
[x + 1 for x in range(10) if x %2 == 0] # 列表生成式 y = (x + 1 for x in range(10) if x %2 == 0) # 生成器 # <generator object <genexpr> at 0x000001F93FF9BCF0> next(y) # 指针
- 创建生成器,列表生成式,我们知道,只要将最外一层的中括号,改为小括号,就是生成器.
- 引出生成器:对象,保存元素的算法,同时记录游标的位置
- ⽣成器保存的是算法,每次调⽤ next(y) ,就计算出 g 的下⼀个元素的值,直到计算到后⼀个元素,没有更多的元素时,抛出 StopIteration 的异常。
- 创建一个生成器:
- 1、通过列表生成式来创建
- 2、通过函数创建生成器:(yield)
- 遍历生成器中的元素内容
- 1、通过next(y) # 超出范围会报错
- 2、通过for遍历
- 3、通过object内置函数:__next__进行输出 # 超出范围会抛出异常
- 4、send函数 # 但生成器的第一个值必须是send(None),后面的send()里面的值无所谓,但不能为空,且会被打印出来
- 可以通过列表进行输出、查看:print(list((i+1 for i in range(1,11))))
如果推算的算法比较复杂,用类似列表生成式的 for 循环无法实现的时候,还可以用函数来实现。比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个 数都可由前两个数相加得到([1, 1, 2, 3, 5, 8, 13, 21, 34, ...]):
仔细观察,可以看出,fib_a函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任 意的元素,这种逻辑其实非常类似generator。也就是说,上面的函数和generator仅一步之遥。要把 fib 函数变 成generator,只需要把 print(b) 改为 yield(b) 就可以了:
def fib_a(times):
# 初始化
n = 0
a, b = 0, 1
while n < times:
yield(b) # 改为生成器 # print(b)
a, b = b, a + b
n += 1
return 'done'
z = fib_a(5)
print('返回值', z)
for i in fib_a(5):
print(i)
在上⾯fib 的例⼦,我们在循环过程中不断调⽤ yield ,就会不断中断。当然要给循环设置⼀个条件来退出循环,不然就会产⽣⼀个⽆限数列出来。同样的,把函数改成generator后,我们基本上从来不会⽤ next() 来获取下⼀个返回值,⽽是直接使⽤ for 循环来迭代:
但是⽤for循环调⽤generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获 StopIteration错误,返回值包含在StopIteration的value中:
def fib_a(times):
# 初始化
n = 0
a, b = 0, 1
while n < times:
yield(b) # 改为生成器
a, b = b, a + b
n += 1
return 'done'
z = fib_a(5)
print('返回值', z) # 返回值 <generator object fib_a at 0x000001F9410399E0>
while True:
try:
x = next(z)
print("value:%d" % x) # value:5
except StopIteration as e:
print("⽣成器返回值:%s" % e.value) # ⽣成器返回值:done
break
这样的到结果,是可以拿到返回值的,我们通过有限的值捕获异常,拿到return的值.
⽣成器是这样⼀个函数,它记住上⼀次返回时在函数体中的位置。对⽣成器函数的第⼆次(或第 n 次)调⽤跳转⾄该函数中间,⽽上次调⽤的所有局部变量都保持不变。⽣成器不仅“记住”了它数据状态;⽣成器还“记住”了它在流控制构造(在命令式编程中,这种构造不只是数据值)中的位置。⽣成器的特点:
-
1. 节约内存
-
2. 迭代到下⼀次的调⽤时,所使⽤的参数都是第⼀次所保留下的,在整个所有函数调⽤的参数都是第⼀次所调⽤时保留的,⽽不是新创建的.
二、迭代器 (Iterator)
迭代是访问集合元素的⼀种⽅式。迭代器是⼀个可以记住遍历的位置的对象。迭代器对象从集合的第⼀个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。
可以被next()函数调⽤并不断返回下⼀个值的对象称为迭代器:Iterator.
list 、 dict 、 str 不是 Iterator(迭代器) ,因为Python的 Iterator 对象表示的是一个数据流,Iterator对象可以被 next() 函数调用并不断返回下一个数据,直到没有数据时抛出 StopIteration 错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过 next() 函数实现按需计算下一个数据,所以 Iterator 的计算是惰性的,只有在需要返回下一个数据时它才会计算。
- 1. 节约内存
- 2. 迭代到下⼀次的调⽤时,所使⽤的参数都是第⼀次所保留下的,在整个所有函数调⽤的参数都是第⼀次所调⽤时保留的,⽽不是新创建的
- 注意 Iterator 对象和 Iterable 对象,一个是迭代器,一个是可迭代对象
- 所以可作用于 for 循环的对象都是 Iterable 类型;
- 可作用于 next( ) 函数的对象都是 Iterator 类型,它们表示一个惰性计算的序列;
- 集合数据类型list 、 dict 、 str 等是 Iterable (可迭代对象)但不是 Iterator ,不过可以通过 iter( ) 函数获得⼀个 Iterator 对象。
-
Iterator 甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的
- 通过迭代器读取大文件
def read_lines(f, newline):
buf = ''
while True:
while newline in buf:
# 找到字符串位置
pos = buf.index(newline)
yield buf[:pos]
buf = buf[pos + len(newline):]
# 读取该长度的字符串
chunk = f.read(512)
if not chunk:
# 文件读取完成
yield buf
break
buf += chunk
with open('NumPy作业答案.txt', 'r', encoding='utf-8') as f:
# 用||分隔段落,记录位置
for line in read_lines(f, '||'): # f 为打开的文件
print(line)
三、高阶函数 (Higher-order function)
- 函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。
- 而函数式编程(请注意多了一个“式”字)——Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近抽象的计算。
- 我们首先要搞明白计算机(Computer)和计算(Compute)的概念。
在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是贴近 计算机的语言。而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。 - 函数式编程就是一种抽象程度很高的编程范式。
- 函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
- Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。
- 函数名也可以是变量
- 函数是由def定义,函数名,括号,括号参数,冒号,函数体组成
- 函数名是指向函数的变量,例如abs()这个函数,可以将abs看成变量,它指向一个可以求绝对值的函数,如果把abs指向其他的对象,就会失去原有的功能.
我们想到,函数可以传参数,而函数名可以做变量,那我们函数里面的参数,也可以传入函数名
那我们可以看到,将函数作为参数,传给另一个参数就是高阶函数.
def add_1(i):
return i*2
def total(x, y, fun):
return fun(x) + fun(y)
add_sum = total(1, 2, add_1)
print(add_sum)
1、map函数
把一个可迭代对象中的每个元素转换为一个新的对象,最后返回一个新的可迭代对象。
来看一下map函数的参数与返回值:
map(func, *iterables) --> map object
def fun(i):
return i*3
for i in map(fun, [1, 2, 3]):
print(i) # 3, 6, 9
map()参数详解:
- func:代表传入参数为函数,这里的函数指定指向函数的函数名,
- iterables:代表参数指定的可迭代的, 列表等
- 返回值:返回处理好的数据
- map()函数:是将传入的func函数作用于,可迭代的数据里面每个元素,并将处理好的新的结果返回
map( ) 传入的第一个参数是 fun_a ,即函数对象本身。由于结果 list_a 是一个 Iterator , Iterator 是惰性序列,因此通过 list() 函数让它把整个序列都计算出来并返回一个list。
很多情况下,也可以使用for循环也可以解决问题,但实际上map作为高级函数,将运算抽象化,还可计算复杂的函数,例如将列表的元素int类型转换为int类型,只需要一行代码。
list(map(str, [1, 2, 3, 4, 5])) # ['1', '2', '3', '4', '5']
2、reduce函数
reduce函数:把一个可迭代对象中的每个元素做聚合处理,最后返回一个聚合之后的值。
from functools import reduce
list_a = [1, 2, 3, 4, 5]
def fun_b(x, y):
return x + y
reduce(fun_b, list_a) # 运算结果如下 15
reduce() 参数详解:
- function:一个有两个参数的函数, 聚合方式
- sequence:是一个序列,是一些数据的集合,或者是一组数据,可迭代对象
- initial:可选,初始参数
- 返回值:返回函数计算的结果
- reduce()函数, 使用function函数(有两个参数)先对集合中的sequence第 1、2 个元素进行操作,如果存在。
- initial参数,则将会以sequence中的第一个元素和initial作为参数,用作调用,得到的结果再与sequence中的下一个数据用 function 函数运算,最后得到一个结果。
依次按照顺序从列表list_a中提取两个元素作为参数,进入fun_b中进行运算,得到的结果,作为下次运算时的其中一个参数,再从列表中取出一个元素,再进行运算。最终得到的结果是总和的计算。
3、filter函数
Python内建的 filter() 函数用于过滤序列,和 map() 类似, filter() 也接收一个函数和一个序列
但是不同的是 filter() 把传入的函数依次作用于每个元素,然后根据返回值是 True 还是 False 决定元素的保留与丢弃
filter的参数: filter(function, iterable)
def not_odd(num):
return num % 2 == 0
newlist = filter(not_odd, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
print(newlist) # <filter object at 0x000001F9419D7C40>
print([i for i in newlist]) # [2, 4, 6, 8, 10]
参数列表:
- function:判断函数。
- iterable:序列,(可迭代对象)。
- 返回值:返回列表
- filter函数,序列(可迭代对象)的每个元素作为参数传递给函数进行判断,然后返回 True 或 False,最后将返回 True 的元素放到新列表中
这里filter函数的两个参数第一个是过滤方法,第二个是需要过滤的列表,将列表里面的元素依次带入函数中进行运算,得到的结果如果为True时,将此结果作为新的filter对象保留,等待函数里面的列表执行完成后,返回最终的值,这里的值为列表,也就是过滤掉了False的数据或元素。
那可以用filter来计算素数,计算素数的其中一个方法是埃氏筛法。 给出要筛数值的范围n,找出以内的素数。先用2去筛,即把2留下,把2的倍数剔除掉;再用下一个质数,也就是3 筛,把3留下,把3的倍数剔除掉;接下去用下一个质数5筛,把5留下,把5的倍数剔除掉;不断重复下去......
用Python来实现这个算法,我们先写一个生成器构造一个从3开始的无限奇数序列,首先偶数列先排除
def oddnum():
n = 1
while True:
n = n + 2
yield n
def undivisible(n):
print('迭代', n)
return lambda x: x % n > 0
def primes():
yield 2
it = oddnum() # 初始序列
while True:
n = next(it) # 返回序列的第一个数
yield n
it = filter(undivisible(n), it) # 构造新序
for n in primes():
if n < 100:
print(n)
else:
break
生成器与迭代器、匿名函数、filter函数,其中:
- 第一段代码生成了以3开始的奇数序列
- 第二段代码自定义过滤函数,包含匿名函数,判断值的取余是否能被整除
- 第三段代码用来返回素数,这里先返回一个2为素数,因为偶数都被排除了所以整除3为基础进行排除,将数据不断 地迭代生成,留下对应的素数序列。
- 那这里就对应的filter函数就是用来过滤的方法进行返回数据。
3.1、max and min
# print(max(emps,key=lambda x:x['salary']))
3.2、sorted函数
把一个可迭代对象里面的每个元素做排序,返回一个列表
# 根据员工的年龄降序排序:print(sorted(emps,key=lambda x : x['age'],reverse=True))
4、匿名函数
lambda [list] : 表达式
我们可以看到对于比较单行返回的函数,使用 lambda 表达式可以省去定义函数的过程,让代码更加简洁,针对不需要多次复用的函数,使用 lambda 表达式可以在用完之后立即释放,提高程序执行的性能。而且还能配合其他的一些高阶函数配合使用。
语法:lambda [list] : 表达式
lambda 参数列表:
- [list]:表示参数列表
- 注意:参数与表达式之间需要冒号来区分
- 表达式 :表达式方法非常多,表达形式也非常多
- 返回值 :为表达式的结果value
对于比较单行返回的函数,使用 lambda 表达式可以省去定义函数的过程,让代码更简洁,针对不需要多次复用的函数,使用 lambda 表达式可以在用完之后立即释放,提高程序执行的性能。而且还能配合其他的一些高阶函数配合使用。
def add(x, y):
return x+ y
print('add_1', add(3,4)) # add_1 7
add_2 = lambda x,y:x+y
print('add_2',add_2(3, 4)) # add_2 7
5、返回函数
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。我们在操作函数的时候,如果不需要立刻求和,而是在后面的代码中,根据需要再计算
- 当我们调用 sum_fun_b() 时,返回的并不是求和结果,而是求和函数 sum_a , 当我们在调用sum_fun_b函数时将他赋值给变量, args参数*args:这里就用到了args参数,它可以将用户指定的任意多个参数打包为一个元组传到函数中进行进一步运算
def sum_fun_a(*args):
a = 0
for n in args:
a = a + n
return a
sum_fun_a(1, 2, 3, 4, 5) # 15
- 当我们调用 sum_fun_b() 时,返回的并不是求和结果,而是求和函数 sum_a , 当我们在调用sum_fun_b函数时将他赋值给变量
def sum_fun_b(*args):
def sum_a():
a = 0
for n in args:
a = a + n
print('a, n:', a, n) # a, n: 15 5
return a
return sum_a
sum_fun_b(1, 2, 3, 4, 5)() # 15
- 而且我们看到创建的两个方法相互不影响的,地址及值是不相同的
- 在函数 sum_fun_b 中又定义了函数 sum_a,并且,内部函数 sum_a 可以引用外部函数 sum_fun_b 的参数和局部
- 变量,当 sum_fun_b 返回函数 sum_a 时,而对应的参数和变量都保存在返回的函数中,这里称为 闭包 。
6、闭包
此时,内部函数对外部函数作⽤域⾥变量的引⽤(⾮全局变量),则称内部函数为闭包。这里闭包需要有三个条件
- 1)必须有一个内嵌函数(函数里定义的函数)——这对应函数之间的嵌套
- 2)内嵌函数必须引用一个定义在闭合范围内(外部函数里)的变量——内部函数引用外部变量
- 3)外部函数必须返回内嵌函数——必须返回那个内部函数
def fun_a(num_a):
def fun_b(num_b): # 内嵌函数
print("b number_in is %d" % num_b) # b number_in is 10
return num_a + num_b
return fun_b
# 给fun_a函数赋值,这个10就是给参数num
ret = fun_a(10)
print(ret(10)) # 20
print(ret(90)) # 100
当一个函数在本地作用域找不到变量申明时会向外层函数寻找,这在函数闭包中很常见,但是在本地作用域中使用的变量后,还想对此变量进行更改赋值就会报错
nonlocal声明
def test():
count = 1
def add():
nonlocal count
print(count) # 1
count += 1
return count
return add
test()() # 2
- nonlocal声明的变量不是局部变量,也不是全局变量,而是外部嵌套函数内的变量。
- 如果从另一个角度来看我们给此函数增加了记录函数状态的功能。当然,这也可以通过申明全局变量来实现增加函数状态的功能。当这样会出现以下问题:
- 1) 每次调用函数时,都得在全局作用域申明变量。别人调用函数时需查看函数内部代码
- 2)当函数在多个地方被调用并且同时记录着很多状态时,会造成非常地混乱
def line_conf(a, b):
def line(x):
return a * x + b
return line
line1 = line_conf(1, 1)
line2 = line_conf(4, 5)
print(line1(5)) # 6
print(line2(5)) # 25
- 使用nonlocal的好处是,在为函数添加状态时不用额外地添加全局变量,因此可以大量地调用此函数并同时记录着多个函数状态,每个函数都是独立、独特的。针对此项功能其实还个一个方法,就是使用类,通过定义__call__ 可实现在一个实例上直接像函数一样调用
- 1.闭包似优化了变量,原来需要类对象完成的⼯作,闭包也可以完成
- 2.由于闭包引⽤了外部函数的局部变量,则外部函数的局部变量没有及时释放,消耗内存
def fun_a():
fun_list = []
for i in range(1, 4):
def fun_b():
return i * i
fun_list.append(fun_b)
return fun_list
f1, f2, f3 = fun_a()
print(f1(), f2(), f3()) # 9,9,9
实际结果并不是我们想要的1,4,9,而是9,9,9,这是为什么呢?
- 这是因为,返回的函数引用了变量 i ,但不是立刻执行。等到3个函数都返回时,它们所引用的变量 i 已经变成了3
- 每一个独立的函数引用的对象是相同的变量,但是返回的值时候,3个函数都返回时,此时值已经完整了运算,并存储,当调用函数,产生值不会达成想要的,返回函数不要引用任何循环变量,或者将来会发生变化的变量,
7、装饰器
1、那什么是装饰器
import time
print('时间是:%s'%time.strftime('%H:%M:%S',time.localtime())) # 时间是:15:42:25
def decor_a(func):
def decor_b():
print("I am decor_b")
func()
print("I am func")
return decor_b
def decor_c():
print("I am decor_c")
decor_a(decor_c)() # 以上函数全数执行
- 装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。
- 它经常用于有以下场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计
2、装饰器的作用就是为已经存在的对象添加额外的功能
def decor_a(fun):
def decor_b():
print('I am decor_b')
fun()
print('I am fun')
return decor_b
@decor_a
def decor_c():
print('I am decor_c')
decor_c() # == decor_a(decor_c)()
print(decor_c.__name__) # outputs: decor_b
- 我们没有直接将decor_c函数作为参数传入decor_a中,只是将decor_a函数以@方式装饰在decor_c函数上。
- 也就是说,被装饰的函数,函数名作为参数,传入到装饰器函数上,不影响decor_c函数的功能,再次基础上可以
- 根据业务或者功能增加条件或者信息。
- (注意:@在装饰器这里是作为Python语法里面的语法糖写法,用来做修饰。)
- 但是我们这里就存在一个问题这里引入魔术方法 __name__ 这是属于 python 中的内置类属性,就是它会天生就存在与一个 python 程序中,代表对应程序名称,一般一段程序作为主线运行程序时其内置名称就是 __main__ ,当自己作为模块被调用时就是自己的名字
3、带参装饰器
- 我们也看到装饰器wraps也是带参数的,那我们是不是也可以定义带参数的装饰器尼,我们可以使用一个函数来包裹装饰器,调入这个参数。
8、偏函数
Python的 functools 模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。要注意,这里的偏函数和数学意义上的偏函数不一样。
-
int() 函数还提供额外的 base 参数,默认值为 10 。如果传入 base 参数,就可以做进制的转换
int('12345', base=8) # 5349
- 在介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。
- 例如:int() 函数可以把字符串转换为整数,当仅传入字符串时, int() 函数默认按十进制转换
- 偏函数把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单继续优化,functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义 int_1() ,可以直接使用下面的代码创建一个新的函数 int_1 # int_2 = functools.partial(int,base =2) # 偏函数
- 理清了 functools.partial 的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
- 注意到上面的新的 int_2 函数,仅仅是把 base 参数重新设定默认值为 2 ,但也可以在函数调用时传入其他值实际上固定了int()函数的关键字参数 base.