一、高阶函数
高阶函数看着很高级,但是从数学的角度来看,这可以理解为是一个复合函数,也就是由多个功能函数组成的一个函数。
其准确的定义是,当一个函数接受至少一个函数作为对象,并且返回一个函数作为结果,那么这个函数就是高阶函数
def add(x, y, f):
return f(x) + f(y)
f = abs 在Python中有一些有意思的点:
- 变量可以指向函数,也就是给函数取了个别名,后续可以通过别名来调用函数
- 函数名本身也是变量,也可以改变其指向,这个在别的语言中是不敢想的,别的语言最多只能Overwrite一下,当然也有可能还有我没接触到的。==当然了,因为很多库函数是定义在模块当中的,需要把模块中的函数变量进行重新赋值,就能让其全局生效。
import builtins;
abs = 10
builtins.abs = 10(一)Map/Reduce
- Map( )
map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回.
def f(x):
return x * x
r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
print(list(r)) 由于结果Map返回的r是一个Iterator,Iterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list。
- Reduce( )
reduce()是把一个函数作用在一个序列上,这个函数(值得是参数中的函数)必须接收两个参数,reduce()把结果继续和下一个元素作累积计算。过程为:取列表的前两个元素进行参数函数运算,并且将结果与下一个列表元素进行运算,逐步得到结果。
from functools import reduce
def fn(x, y):
return x * 10 + y
print(reduce(fn, [1, 3, 5, 7, 9]))
# 13579
from functools import reduce
def fn(x, y):
return x * 10 + y
#Map 结合 Reduce
def char2num(s):
digits = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
return digits[s]
print(reduce(fn, map(char2num, '13579')))
# 13579(二)Filter
Filter跟Map类似,都是将传入的函数依次作用于每个元素,但是最后返回的值是True/False决定保留还是丢弃该元素。==而且,filter()使用了惰性计算,所以只有在取filter()结果的时候,才会真正筛选并每次返回下一个筛出的元素(也就是说每次调用只会返回一个,需要循环遍历)==。
计算素数的一个方法是埃氏筛法,它的算法理解起来非常简单:
首先,列出从2开始的所有自然数,构造一个序列:
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取序列的第一个数2,它一定是素数,然后用2把序列的2的倍数筛掉:
3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取新序列的第一个数3,它一定是素数,然后用3把序列的3的倍数筛掉:
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
取新序列的第一个数5,然后用5把序列的5的倍数筛掉:
7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, …
不断筛下去,就可以得到所有的素数。
def _odd_iter(): #构造一个从3开始的奇数序列生成器
n = 1
while True:
n = n + 2
yield n
def _not_divisible(n): #定义一个筛选函数
return lambda x: x % n > 0
def primes(): #定义一个生成器,不断返回下一个素数
yield 2
it = _odd_iter() # 初始序列
while True:
n = next(it) # 返回序列的第一个数
yield n
it = filter(_not_divisible(n), it) # 构造新序列
这个生成器先返回第一个素数2,然后,利用filter()不断产生筛选后的新的序列。
由于primes()也是一个无限序列,所以调用时需要设置一个退出循环的条件:
# 打印100以内的素数:
for n in primes():
if n < 100:
print(n)
else:
break(三)Sorted()
Sorted()函数也是一个高阶函数,它除了正常排序之外,还可以接受一个key函数来实现自定义的排序。
# 正常排序
print(sorted([36, 5, -12, 9, -21]))
# [-21, -12, 5, 9, 36]
# 加上Key值
print(sorted([36, 5, -12, 9, -21], key=abs))
# [5, 9, -12, -21, 36]- key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。==需要注意的是Key值函数的作用是在中间的附加条件,并不会直接改变元素的值,从某种意义上来说,Key值也可以视为排序需忽略的条件==
比如上述例子是让我们忽略元素的正负性,又比如如果是我们排序的是字符串,我们应该忽略大小写,按照字母序排序,这个时候我们可以把字符全部转为大写或者全部转为小写。
print(sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower))
# ['about', 'bob', 'Credit', 'Zoo']
- 如果要进行反向排序,不必改动key函数,可以传入第三参数
reverse=True:
print(sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True))
# ['Zoo', 'Credit', 'bob', 'about']
练习 假设我们用一组tuple表示学生名字和成绩:
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
请用sorted()对上述列表分别按名字排序:
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
def by_name(t):
return t[0]
L2 = sorted(L, key=by_name)
print(L2)
再按成绩从高到低排序:
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
def by_score(t):
return t[1]
L2 = sorted(L, key=by_score)
print(L2) 这里的核心函数很简单,核心机制是因为Key的作用机制详见:Key函数将作用于list的每个元素;所以对于这个练习来说每次作用于一个元素,即元组,那么每次遍历操作的对象其实是一个元组,那么直接用下标调用相应的元素即可。
二、返回函数
高阶函数除了可以接收函数做为参数外,还可以把函数作为结果值进行返回,返回的函数可以是新建的函数,也可以是对现有函数的引用。
返回函数最强大的特性就是他们可以“记住”创建时的环境,我们称之为闭包;
例如:
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
f = lazy_sum(1, 3, 5, 7, 9)
print(f)
# <function lazy_sum.<locals>.sum at 0x101c6ed90>
print(f()) #需要调用函数f的时候,才会真正计算结果
#25 需要注意的是,每次调用lazy_sum()的时候都会返回一个新的函数,与传入的参数相同与否无关。
f1 = lazy_sum(1, 3, 5, 7, 9)
f2 = lazy_sum(1, 3, 5, 7, 9)
print(f1==f2)
# False f1()和f2()的调用结果互不影响。
(一)闭包
说白了,闭包函数“记住了”它被创建时的词法作用域,即使外部函数已经执行完毕,也不会回收“记忆”,也就是说在之后的每次调用的时候都会记住新的环境和值。
例如:
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
f = lazy_sum(1, 3, 5, 7, 9)
print(f)
# <function lazy_sum.<locals>.sum at 0x101c6ed90>
print(f())
# 25
f1 = lazy_sum(1, 3, 5, 7, 9)
f2 = lazy_sum(1, 3, 5, 7, 9)
print(f1==f2)
# False
从line11可以看出f接收到的是一个函数,并未收到函数的运算结果。直到line13的时候,调用f函数,才会真正的返回函数运算值。
同时值得注意的是,在line18的时候我们可以看到进行布尔运算的时候返回值为false,这意味着,每次调用lazy_sum()都会返回一个新的函数,即使传入的参数是相同。f1和f2的调用结果都是相互独立、互不影响的。
注意:返回闭包的时候要牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。为什么呢?例:
#错误示范
def count():
fs = []
for i in range(1, 4):
def f():
return i*i
fs.append(f)
return fs
f1, f2, f3 = count()
因为,每次返回的f函数都是返回的i的引用,可以理解成是调用的那一片地址空间,由于是闭包函数,f()才是真正的调用,所以函数在定义时没有调用,一直到最后才调用,此时的i已经变成3了,每次的引用都是同一个i所以,最后返回的都是最后一次的值。
那如果一定要引用循环变量怎么办?Solution:方法是再创建一个函数,用该函数参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变。
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 缺点是代码较长,可利用lambda函数缩短代码
(二)nonlocal
使用闭包,就是内层函数引用了外层函数的局部变量。
如果只是读外层变量的值,我们会发现返回的闭包函数调用一切正常,但是,如果对外层变量赋值,由于Python解释器会把x当作函数fn()的局部变量,它会报错:
# 只读
def inc():
x = 0
def fn():
# 仅读取x的值:
return x + 1
return fn
f = inc()
print(f()) # 1
print(f()) # 1
#赋值
def inc():
x = 0
def fn():
# nonlocal x
x = x + 1
return x
return fn
f = inc()
print(f()) # 报错
print(f()) # 报错 原因是x作为局部变量并没有初始化,直接计算x+1是不行的。但我们其实是想引用inc()函数内部的x,所以需要在fn()函数内部加一个nonlocal x的声明。加上这个声明后,解释器把fn()的x看作外层函数的局部变量,它已经被初始化了,可以正确计算x+1。
Summary:使用闭包时,对外层变量赋值前,需要先使用nonlocal声明该变量不是当前函数的局部变量。
三、匿名函数
语法:lambda arguments:expression
- arguments是函数的参数,可以是一个,也可以是多个,当有多个的时候用逗号分隔。
- expression是一个表达式,其计算的结果就是函数的返回值。
当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。
匿名函数有个限制,==就是只能有一个表达式,不用写return,返回值就是该表达式的结果==。
用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数。
f = lambda x: x * x
print(f)
##<function <lambda> at 0x101c6ef28>
print(f(5))
##25
同时,也能把匿名函数作为返回值返回,比如:
def build(x, y):
return lambda: x * x + y * y
四、装饰器
当我们需要增强一个函数的功能,比如在函数调用前后自动打印日志,但又不希望修改函数的定义,这种在代码运行期间动态增加功能的方式,称为装饰器。
本质上,decorator就是一个返回函数的高阶函数。例:
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper 观察上面的log(),因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:
@log
def now():
print('2024-6-1')
把@log放到now()函数的定义处,相当于执行了语句:
now = log(now)sd
五、偏函数