有时候看了不少书、文档、博客,有的知识我以为自己已经掌握了,但让我用自己的话讲出来却全无思路,只能吐出几个关键概念,所看的东西并没有真正形成体系。学习编程的过程中我相信这样一种检验标准——能讲得出才是真的懂。
我想要达到的标准是能用自己的话把一个知识点的整个流程向一个不懂的人讲明白,所以平时如果有片段化的空闲时间,我都会在脑海里,或者对着空气向一个“不存在的人”讲我最近学习的一些知识来检验是否理解,或者复习之前学过的一些东西。比如睡前躺在床上、坐公交车、或者等人,我会找一个知识点比如关于python的:装饰器、迭代器和生成器、python 多线程,多进程,常用的数据结构列表,元组,字典的一些特性,上下文管理器等,然后花十几分钟时间,把这个知识点相关的体系,流程,实现原理等用自己的话复述出来,或者记在手机记事本里(这样对以后自己复习也会更友好)。
这里记录一些我对自己“讲课”的思路吧。
装饰器
python中的装饰器本质上是一个函数,这个函数接收一个函数(广义上可以是任何可调用对象)返回一个新函数,这个新函数和被装饰的函数有相同的行为:接收的参数格式相同,返回原函数本应返回的值,并且通常还会做一些额外的工作:比如执行原函数时打印执行日志,装饰协程函数自动调用next()预激协程。
def decorator(func):
print('这里在被装饰的func定义时执行')
@functools.wraps(func)
def inner(*args, **kwargs):
print('这里在被装饰func调用时,在真正执行func之前执行')
return func(*args,**kwargs)
print('这里在被装饰func调用时,func执行完毕之后执行')
return inner
@decorator
def func():
pass
这是装饰器的典型用法,@decorator相当于执行了
func = decorator(func)
也就是说我们的func
已经被狸猫换太子了,原本的变量名func
指向的是Function: func, Parent = [Global]
,被装饰之后,变量名func
指向的是Function: inner, Parent = [decorator]
,是和原本不同的函数对象,原函数对象的__name__
、__doc__
等许多属性都被掩盖了,为了尽量保持装饰前后的一致性,python提供了一个装饰器用于帮我们自动统一inner函数的各种属性。
1.装饰器实现的是“狸猫换太子”,使用@functools.wraps(func)
可以让“狸猫”更像“太子”。
2.装饰器函数在被装饰的函数定义时就立刻执行,被装饰的函数在调用时才会执行。
3.如果想动态生成一个装饰器函数,可以在decorator函数外再嵌套一个外层函数,外层函数接收参数动态地生成一个装饰器函数。
4.标库中一个很有用的装饰器:functools.lru_cache
可以自动记录递归的每一个值,方便在以后的递归中复用结果,可以明显提升慢速递归(比如递归版斐波那契数列)的执行效率。
5.装饰器本质上是函数的嵌套,用到了一个概念:闭包,可以利用闭包的特性实现一些功能。
闭包
闭包实现的是函数作用域的延伸
闭包发生于函数的嵌套,一个locally def的函数始终有权访问父函数的参数和变量。比如一个统计执行次数的装饰器函数:
def decorator(func):
x = [0]
def inner(*args, **kwargs):
x[0] += 1
print('这是本函数的第%s次执行'%x[0])
return func(*args,**kwargs)
return inner
decorator函数内加一个局部变量,然后decorator函数定义了一个inner并return,return代表decorator已经执行完毕,意味着它的作用域已经消失。但实际上inner函数的作用域继承了其父函数的作用域,所以在inner函数中我们仍然能不断引用x实现计数。
这就是所谓作用域的延伸,一个函数的作用域除了包括它自身的Local frame外,还包括Global frame,如果这是一个嵌套的内部函数,这个frame链又会延伸它父函数的frame,
如果有多层嵌套?每一层父函数的frame最终都会被inner继承。当函数引用了某个变量,解释器便会沿着从Local到Global逐级查找,直到找到这一变量为止。
作用域延伸的实现,将父函数中的变量作为“自由变量”保存在inner函数这一对象的某个属性中。
一个简单的例子,这个函数可能很多人看一眼凭直觉能断定输出的结果,但具体的机制却未必能说明白吧:
a = 0
def outter():
b = 1
def middle():
c = 2
def inner():
d = 3
print(a) # 全局变量
print(b) # 自由变量
print(c) # 自由变量
print(d) # 局部变量
return inner
return middle
k = outter()()
print('自由变量:', k.__code__.co_freevars)
k()
结果
自由变量: ('b', 'c')
0
1
2
3
内部定义的函数其作用域会链式延伸!父函数中的参数和变量被以自由变量的形式储存了下来,这样即使父函数执行了return,作用域已经消失,内部函数仍让能继续引用父函数中的变量和参数。
同时python的参数机制有一个特性:函数定义体中如果发生了一次赋值行为,那么python会认为这是一个局部变量。
这段代码如果调用k(),会报错:在赋值前进行了引用。
def outter():
b = 1
def middle():
print(b)
b = 2
return middle
k = outter()
print('自由变量:', k.__code__.co_freevars)
因为有b的赋值语句存在,python会在执行print(b)之前便断定b是一个局部变量,导致b不再是一个自由变量,print(b)找不到b导致报错。这个执行顺序看似不合逻辑,命名赋值行为明明发生在后,凭啥我print(b)的时候就不让过了呢?具体的执行顺序可以阅读python的字节码。
所以我们得出结论使用自由变量的前提是:不要赋值!
这里要注意python的一个隐式赋值特性:
对不可变对象调用 +=, -=等增量操作符。
a为不可变对象时,a += b执行的是赋新值 a = a + b,调用的是a.__add__(b)
a为可变对象时,a += b 实现的是就地修改,调用的是 a.__iadd__(b)
一个可以用于统计执行次数的装饰器修改自由变量是这么写的:
x[0] += 1
这里使用的是x[0] += 1,背后执行的是什么呢?
我们仅仅调用列表对象x的__setitem__
方法就地修改了x,把这个列表的第0项改变了而已,并没有执行赋值操作,所以x仍然是一个自由变量!把要修改的值作为可变对象的元素或是以对象的属性保存,这是python 2 保护自由变量的典型方法。
python 3有另一个机制nonlocal
声明来“保护”自由变量,nonlocal x
的意思简洁明了,声明x不是一个局部变量,我就算对x赋值也别管闲事。
这样我们的装饰器就可以写为:
def decorator(func):
x = 0
def inner(*args, **kwargs):
nonlocal x
x += 1
print('这是本函数的第%s次执行'%x[0])
return func(*args,**kwargs)
return inner