一、高阶函数

​ 高阶函数看着很高级,但是从数学的角度来看,这可以理解为是一个复合函数,也就是由多个功能函数组成的一个函数。

​ 其准确的定义是,当一个函数接受至少一个函数作为对象,并且返回一个函数作为结果,那么这个函数就是高阶函数

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()函数接收两个参数,一个是函数一个是Iterablemap将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回.

def f(x):
    return x * x
 
 
r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9]) 
print(list(r))

​ 由于结果Map返回的r是一个IteratorIterator是惰性序列,因此通过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

五、偏函数