1 高阶函数
在Python中一切皆对象,当然也包括函数。函数在Python中是一等公民
(First Class Object)。即函数与字符串数组整型无异,它可以被命名可以被赋值可以当作参数被传进另一个函数也可以被另一个函数当作返回值可以放在任何位置,简单来说:
- 函数也是一个对象,并且是一个可调用对象(callable)
- 函数可以作为普通变量、参数、返回值等等
那什么是高阶函数呢?在Python中我们可以理解为:当一个函数接受另一个函数作为参数使用,或者一个函数最后返回了另外一个函数。在这两种情况下,这个函数就可以称之为高阶函数(满足以上任意一种情况即可,不必同时满足)。
数学概念: y = g(f(x))
def outer(x):
def inner(step=1):
nonlocal x # 声明局部变量x不是一个局部变量,应该在外层寻找
x += step
return x
return inner
foo1 = outer(10)
foo2 = outer(10)
print(foo1())
print(foo2())
内层函数inner还引用了外层函数的自由变量x,形成了闭包,由于外部函数返回了内部函数,所以这就是一个典型的高阶函数。针对上面实例还需要说明是:
- ounter函数每调用一次生成的都是一个新False的函数对象
- foo1和foo2也都是相互独立的两个函数
In [1]: def outer(x):
...: def inner(step=1):
...: nonlocal x # 声明局部变量x不是一个局部变量,应该在外层寻找
...: x += step
...: return x
...: return inner
...: foo1 = outer(10)
...: foo2 = outer(10)
...: foo1 is foo2
is
的比较做则是:先比较元素的内存地址,然后比较元素的内容。首先 foo1和foo2 属于不同的对象,所以内存地址肯定不同,然后由于函数对象没有实现函数内容的比较所以这里返回False
1.1 自定义sort函数
从头构建一个类似于内置函数sorted的函数,来体会高阶函数的作用,顺便理解一下sorted的原理。
1.1.1 将规模缩小,先实现排序,先不管key和reverse参数
def sort(iterable, *, key=None, reverse=False):
new_list = []
for value in iterable:
for i, k in enumerate(new_list):
if value < k:
new_list.insert(i, value)
break
else:
new_list.append(value)
return new_list
print(sort([1,6,2,7,9,3,5]))
分析:
- sort返回一个新的列表,我们这里构建一个列表,用于存放排序好的列表
- 为了练习算法,这里选择使用直接插入排序,注意:直接插入排序是原地排序,这里是变化,直接插入到新的已排序列表中去
- 利用enumerate构建索引,用于确认插入位置,注意:因为new_list在我们这个场景下是有序的!所以才可以这样用
- 初始情况下,由于new_list为空,所以待排序列表的第一个元素会被直接插入到已排序列表的首位
- 后续只需在待排序的列表中,拿出一个数据,和已排序区的元素,从左至右依次对比即可
- 如果大于已排序区的所有元素,那么直接追加即可,如果小于某一个元素只需要在对应的元素为插入待排序元素即可
- 如果小于的话,因为列表的特性,在一个位置上插入数据,那么原数据会自动向右移动,所以符合直接插入排序的原理
1.1.2 添加reverse参数判断
def sort(iterable, *, key=None, reverse=False):
new_list = []
for value in iterable:
for i, k in enumerate(new_list):
flag = value > k if reverse else value < k
# if reverse:
# flag = value > k
# else:
# flag = value < k
if flag:
new_list.insert(i, value)
break
else:
new_list.append(value)
return new_list
# return new_list[::-1]
print(sort([1,6,2,7,9,3,3,5]))
分析:
- 倒序有两种表达方式,即正序排好,然后倒着截取。或者是按照倒序排列,这里使用倒序排列。
- 正序时:如果value大于k,那么需要把value插入到k的位置上
- 倒序时,如果value小于k,那么需要把value插入到k的位置上
- 所以添加flag来采集用户传入的reverse参数,这里使用了三元表达式来简化代码
1.1.3 添加key参数判断
def sort(iterable, *, key=None, reverse=False):
new_list = []
for value in iterable:
value_new = key(value) if key else value
for i, k in enumerate(new_list):
k_new = key(k) if key else k
flag = value_new > k_new if reverse else value_new < k_new
if flag:
new_list.insert(i, value)
break
else:
new_list.append(value)
return new_list
# return new_list[::-1]
print(sort(['a',1,2,'b'], key=str, reverse=True))
分析:
- key传入了一个函数,用于对每个key进行转换,然后使用转换后的元素来进行比较,所以我们这里使用了转化后的变量来比较
- 当key可以被调用,那么我们认为它是一个函数,那么调用他对元素进行转换。
- 这里传入了str函数,如果结合前面所学的知识可以改为lambda表达式。
def sort(iterable, *, key=None, reverse=False):
new_list = []
for value in iterable:
value_new = key(value) if callable(key) else value
for i, k in enumerate(new_list):
k_new = key(k) if callable(key) else k
flag = value_new > k_new if reverse else value_new < k_new
if flag:
new_list.insert(i, value)
break
else:
new_list.append(value)
return new_list
print(sort(['a',1,2,'b'], key=lambda x:str(x), reverse=True))
- 传参时利用了lambda表达式,在函数内部,每次传入一个参数,返回它的str对象,其实效果等同于str,这里只是提一下lambda表达式。
1.2 内建函数(高阶函数)
Python内置了很多高阶函数的应用,这里仅介绍较为常用的。
- sorted: 排序函数,直接返回新的列表
- filter:过滤函数,返回一个迭代器
- map:映射函数,返回一个迭代器
- zip:拉链函数,返回一个迭代器
1.2.1 sorted排序
sorted(iterable, /, *, key=None, reverse=False)
立即返回一个新的列表,对一个可迭代对象的所有元素排序。
key
: 排序规则为key定义的函数reverse
: 表示是否进行翻转排序
In [49]: lst
Out[49]: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
In [50]: import random
In [51]: random.shuffle(lst)
In [52]: lst
Out[52]: [70, 45, 90, 40, 30, 80, 25, 55, 5, 75, 85, 95, 50, 20, 35, 15, 10, 60, 65, 0]
In [53]: sorted(lst,reverse=True)
Out[53]: [95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5, 0]
In [54]: lst.append('a')
In [55]: sorted(lst,key=str,reverse=True)
Out[55] :['a', 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 5, 45, 40, 35, 30, 25, 20, 15, 10, 0]
In [58]: sorted(lst,key=lambda x:str(x))
Out[58]: [0, 10, 15, 20, 25, 30, 35, 40, 45, 5, 50, 55, 60, 65, 70 75, 80, 85, 90, 95, 'a']
In [59]: lst.remove('a')
In [61]: sorted(lst,key=lambda x : 100 - x )
Out[61]: [95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5, 0]
- 这里定义了一个接受一个参数的匿名函数,然后返回处理后的元素(
这是匿名函数最常用的场景之一
) - 功能等同于直接调用str函数
sorted直接返回一个新的列表,而列表也有sort方法,通过
list.sort()
进行排序是直接原地进行排序的。
1.2.2 filter 过滤
filter(function or None, iterable) --> filter object
过滤可迭代对象的元素,返回一个迭代器
function
:表示一个函数,它的功能是:每次从可迭代对象iterable取出一个元素交给function函数处理,如果返回True,则保留该元素,否则提出该元素,如果是None,表示剔除等效False的对象iterable
:可迭代对象
In [62]: lst1 = [1,9,55,150,-3,78,28,123]
In [64]: list(filter(lambda x:x%3==0,lst1))
Out[64]: [9, 150, -3, 78, 123]
# 等同于
In [66]: def func(iterable):
...: for i in iterable:
...: if i % 3 == 0:
...: yield i
...:
In [67]: list(func(lst1))
Out[67]: [9, 150, -3, 78, 123]
# 或者
In [66]: def func(iterable):
...: for i in iterable:
...: if (lambda x:x%3==0)(i): ## l这里属于函数调用:func()()
...: yield i
...:
1.2.3 map 映射
map(func, *iterables) --> map object
对多个可迭代对象的元素按照指定的函数进行映射,返回一个迭代器
func
:一个函数,用于处理iterable的元素,在指定多个iterable对象是,函数的参数数量与iterable的数量是相等的。*iterable
:一个或多个可迭代对象
In [69]: list(map(str,range(10)))
Out[69]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
In [70]: list(map(lambda x:x+1,range(10)))
Out[70]: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
In [73]: dict(list(map(lambda x,y:(x,y),'abcde',range(10))))
Out[73]: {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4}
In [74]: dict(map(lambda x:(x%5,x),range(500)))
Out[74]: {0: 495, 1: 496, 2: 497, 3: 498, 4: 499}
map对象的长度,等同于最小的iterable长度。(木桶效应)
2 柯里化
在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。-- 来自维基百科的解释
总结一下:柯里化指的就是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。其数学表达式为:
z = f(x, y) 转换成 z = f(x)(y) 的形式
这里把经典的add函数拿来进行转换:
- 想要转换为add(4)(5),那么add(4)必须要返回一个函数,否则无法将5当作参数传入
def add(x, y):
return x + y
def new_add(x):
def inner(y):
return x + y
return inner
print(add(4, 5)) # 9
print(new_add(4)(5)) # 9
print(add(4, 5) == new_add(4)(5)) # True
通过函数嵌套,就可以完成函数的柯里化了,对柯里化有所了解以后,那么我们就可以继续来看Python中对我们小白来说的第一个难点:装饰器
3 装饰器
什么是装饰器?概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。我们接下来从一个需求开始学习装饰器。
3.1 需求分析
现在有如下函数,我们需要将这个函数的日志打印到终端
def add(x,y):
print('我被调用执行啦') # 新增打印日志语句
return x + y
add(100,200)
但是仔细思考,打印日志是一个独立的功能,它和add函数本身并没有什么关联关系,我们说一个函数是为完成一个工程的,所以直接写在函数里面不是不可以,但是不建议,并且打印日志属于调试信息功能,与业务无关,不应该放在业务函数加法中。
如果不需要写在函数的里面,那么我们得想办法写在函数的外面
def add(x, y):
return x + y
def logger(fn, x, y):
print('函数开始执行')
res = fn(x, y)
print('函数执行完毕')
return res
logger(add,4,5)
- 这样就解决了打印语句在函数内定义的问题了。
- 但是如果add函数的形参很多,我们要挨个写上吗?所以*args,**kwargs帮了我们很大的忙
def add(x, y):
return x + y
def logger(fn, *args, **kwargs):
print('函数开始执行')
res = fn(*args, **kwargs)
print('函数执行完毕')
return res
logger(add,4,5)
3.2 函数柯里化
使用logger(add,4,5)
来调用我们的add函数真是太丑了,给其他人看,可能人家也不知道你要干啥,结合前面所学的柯里化,我们进行如下变动
def add(x, y):
return x + y
def logger(fn):
def wrapper(*args, **kwargs):
print('函数被执行了')
res = fn(*args, **kwargs)
print('函数执行完毕')
return res
return wrapper
logger(add)(4,5)
这样看起来是不是就好看多了?当指定logger(add)(4,5)时,才会打印日志,但如果想要在所有调用add函数的地方,我们还需要在所有调用add的地方修改为logger(add)(参数),想一想,如果我能把logger(add)变成add是不是就可以直接写成add(4,5)了呢?
logger(add)(4,5)
--------
add = logger(add)
add(4,5) # 将add重新指向了新的函数wrapper
按照柯里化的原型中logger(add)返回了一个函数wrapper,而我们的(4,5)其实是传递给了wrapper,结合我们前面所学的高阶函数,这里的wrapper,是一个闭包函数,因为在内部对fn进行了执行,而且增加了打印日志的功能,我们在执行wrapper的同时,也会执行原来的函数fn,并且添加了打印日志的功能,所以logger就是一个装饰器函数
!!!
3.3 装饰器函数(语法糖)
语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·蘭丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。 语法糖让程序更加简洁,有更高的可读性。Python针对我们刚刚编写的logger(add)函数,进行了语法糖优化,所以下面是我们使用语法糖之后的
def logger(fn):
def wrapper(*args, **kwargs):
print('函数被执行了')
res = fn(*args, **kwargs)
print('函数执行完毕')
return res
return wrapper
@logger # 等于 add = logger(add)
def add(x, y):
return x + y
add(4,5)
当解释器执行到
@logger
时,会自动把它下面的函数当作参数,传给logger函数,所以这里@logger
其实就等于add = logger(add)
另外,logger必须要定义在add函数之前才可以被装载!这一点很重要!
3.4 装饰器带来的问题
利用装饰器计算如下函数的运行时间
import time
import datetime
def logger(fn):
def wrapper(*args, **kwargs):
start = datetime.datetime.now()
res = fn(*args, **kwargs)
total_seconds = (datetime.datetime.now() - start).total_seconds()
print('函数:{} 执行用时:{}'.format(wrapper.__name__,total_seconds))
return res
return wrapper
@logger
def add(x, y):
time.sleep(2)
return x + y
执行结果:
In [76]: add(4,5)
函数:wrapper 执行用时:2.000944
这里
__name__
表示函数的名称.
什么鬼?这里为什么打印的是wrapper啊,为什么不是add呢?这样的话,别人不就发现我把这个函数给偷偷换掉了吗?不行不行,我得想个办法把函数的属性复制过来,由于这个功能和打印用时的装饰器不是一个功能,那么我们还得给装饰器另加一个装饰器。-_-!
import time
import datetime
def copy_properties(old_fn):
def wrapper(new_fn):
new_fn.__name__ = old_fn.__name__
return new_fn
return wrapper
def logger(fn):
@copy_properties(fn) # wrapper = copy_properties(fn)(wrapper)
def wrapper(*args, **kwargs):
start = datetime.datetime.now()
res = fn(*args, **kwargs)
total_seconds = (datetime.datetime.now() - start).total_seconds()
print('函数:{} 执行用时:{}'.format(wrapper.__name__,total_seconds))
return res
return wrapper
@logger
def add(x, y):
time.sleep(2)
return x + y
add(4,5)
- 解释器执行到
@copy_properties(fn)
时,会把下面的wraper装入,等于wrapper = copy_properties(fn)(wrapper)
- 由于知道了参数的个数(一定是一个函数对象),这里就没有使用*args, **kwargs
- 函数的属性不止__name__一个,其他的怎么办呢?
3.5 拷贝函数属性
Python的内置模块functools中,内置了很多常用的高阶函数,其中wraps就是用来拷贝函数的属性及签名信息的。利用wraps,我们就不需要自己编写copy_properties函数了,下面是修改后的版本
import time
import datetime
import functools
def logger(fn):
@functools.wraps(fn) # wrapper = functools.wraps(fn)(wrapper)
def wrapper(*args, **kwargs):
start = datetime.datetime.now()
res = fn(*args, **kwargs)
total_seconds = (datetime.datetime.now() - start).total_seconds()
print('函数:{} 执行用时:{}'.format(wrapper.__name__,total_seconds))
return res
return wrapper
@logger
def add(x, y):
time.sleep(2)
return x + y
add(4,5)
通过使用 @functools.wraps(fn)
我们可以方便的拷贝函数的属性签名信息,比如:'module', 'name', 'qualname', 'doc','annotations'等,这些属性信息,将在后续部分进行讲解,这里知道即可
4 代参装饰器
上面章节讲到的是带一个参数(函数)的装饰器,在Python中这种装饰器被称为无参装饰器,因为语法糖的表现形式就是 @logger
,下面要说的是代参数的装饰器,即@logger(50)
4.1 还是从一个需求开始
以上述函数为例,我们需要记录当函数执行超过一定时间时的日志信息,该怎么办呢?假设这个时间是5秒,那么很显然,我们需要把这个时间变量传入到装饰器中进行判断。也就是说我们需要写成这种形式:
logger(5)(add)
looger(5)返回的是一个函数,否则无法将add传入
4.2 代参装饰器编写
import time
import datetime
import functools
def logger(var):
def inner(func):
def wrapper(*args, **kwargs):
start = datetime.datetime.now()
res = func(*args, **kwargs)
total_seconds = (datetime.datetime.now() - start).total_seconds()
if total_seconds > var:
print('函数执行时间过长')
return res
return wrapper
return inner
@logger(5) # logger(5)(add)
def add(x, y):
time.sleep(6)
return x + y
是不是很简单? 在掌握了柯里化以及无参装饰器。
4.3 代参装饰器小结
代参装饰器有如下特点:
- 它是一个函数
- 函数作为它的形参
- 返回值是一个不带参数的装饰器函数
- 使用@function_name(参数列表)方式调用
- 可以看做在装饰器外层又加了一层函数
装饰器什么时候被执行?,还记得@logger等于什么吗? add = logger(add) 等号等于赋值,是不是要先计算右边的?所以,装饰器在函数定义阶段就已经被执行了!不是等到被装饰的函数执行时,才执行哦!真正执行时,每次都会生成一个新的wrapper被调用而已!