内包和装饰器
Python和JavaScript(暂时只研究过JS内包😆所以拿来举例)一样都支持内包,而内包又是装饰器的基础,所以本篇文章就简单总结一下本人关于内包和装饰器的理解和使用。
1.内包
在统计或者金融领域,可能需要一个函数实现,输入一个数据,来更新当前的平均值,例如下面的avg
(此例来自fluent python):
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
如果没有内包,我们可能会定义一个如下class来完成该需求:
class Avg():
def __init__(self):
self.num_list = list()
def __call__(self, num):
self.num_list.append(num)
total = sum(self.num_list)
return total/len(self.num_list)
测试效果如下:
>>> from avg import Avg
>>> avg1 = Avg()
>>> avg1(1)
1.0
>>> avg1(9)
5.0
而使用内包之后,我们可以通过如下代码完成该功能:
def average():
num_list = list()
def cal_avg(num):
num_list.append(num)
total = sum(num_list)
return total/len(num_list)
return cal_avg
测试效果:
>>> from avg import average
>>> avg1 = average()
>>> avg1 = average()
>>> avg1(1)
1.0
>>> avg1(2)
1.5
从上面这个例子我们可以看到, 内包实际上是利用python中一切皆对象(包括函数)的特性, 通过average
返回了cal_avg
函数对象,而cal_avg
函数对象还包括其函数域的局部变量信息。那么有疑问了,num_list
并不在cal_avg
的函数域(少年你骗我吧😢), 那是因为num_list
是cal_avg
的自由变量(free variable),指不在本定义域中绑定的变量,可以查看__code__来查看编译后的变量类型:
>>> avg1.__code__.co_varnames
('num', 'total')
>>> avg1.__code__.co_f
avg1.__code__.co_filename avg1.__code__.co_firstlineno avg1.__code__.co_flags avg1.__code__.co_freevars
>>> avg1.__code__.co_freevars
('num_list',)
而num_list
的结果都保存在__closure__:
>>> avg1.__closure__[0]
<cell at 0x10e81f3d8: list object at 0x10ea53cc8>
>>> avg1.__closure__[0].cell_contents
[1, 2]
下面贴出avg1的字节代码:
>>> from dis import dis
>>> dis(avg1)
12 0 LOAD_DEREF 0 (num_list)
2 LOAD_ATTR 0 (append)
4 LOAD_FAST 0 (num)
6 CALL_FUNCTION 1
8 POP_TOP
13 10 LOAD_GLOBAL 1 (sum)
12 LOAD_DEREF 0 (num_list)
14 CALL_FUNCTION 1
16 STORE_FAST 1 (total)
14 18 LOAD_FAST 1 (total)
20 LOAD_GLOBAL 2 (len)
22 LOAD_DEREF 0 (num_list)
24 CALL_FUNCTION 1
26 BINARY_TRUE_DIVIDE
28 RETURN_VALUE
从反汇编结果可以看出,avg1
首先执行对num_list
的解引用,然后执行append吧啦吧啦,我觉得自己表达能力有限(其实是懒),不过代码比较简单,也容易理解吧。
nonlocal
OK,本来我觉得这个求平均值的函数效率太低,想稍微修改一下:
def adv_avg():
cnt = 0
avg = 0
def cal_avg(num):
cnt = cnt + 1
avg = avg + (num - avg)/cnt
return avg
return cal_avg
执行一下咯,尼玛又是一个坑啊:
>>> from avg import adv_avg
>>> avg = adv_avg()
>>> avg(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/xiowang/Documents/Py/decorator/avg.py", line 21, in cal_avg
cnt = cnt + 1
UnboundLocalError: local variable 'cnt' referenced before assignment
为什么会这样呢?原因就是数字,字符串,元组这些不可变类型执行cnt = cnt + 1
时,会创建一个新的cnt
,而不是去引用。因此这时就需要用到nonlocal来显示声明变量是外部定义的:
def adv_avg():
cnt = 0
avg = 0
def cal_avg(num):
nonlocal cnt, avg
cnt = cnt + 1
avg = avg + (num - avg)/cnt
return avg
return cal_avg
OK, 修改之后,再运行,perfect:
>>> from avg import adv_avg
>>> avg3 = adv_avg()
>>> avg3(1)
1.0
>>> avg3(2)
1.5
>>> avg3(3)
2.0
2.装饰器
对于装饰器本人理解并不深,但是从名字理解,装饰器就是给用来点缀函数或者类(都是对象)。我打个比方,古代同样两幅画,经过皇家印章盖个印,那么他添加了皇室属性,那收藏价值必须飙升呀,怎么都值个4,5线城市的小房子啊(咋最近老喜欢说房子,还不敢说上海的房子,泪啊😢),其实装饰器就是干盖章这回事呀,完成比如给函数添加打log,计时等功能。
一个关于装饰很经典的例子,给函数添加计时功能:
from time import time, sleep
def decoTime(func):
def retFunc():
start = time()
func()
now = time()
print("spend time:{:.2f}".format(now-start))
print("func_name: {}".format(func.__name__))
return retFunc
@decoTime
def myfunc():
sleep(2)
myfunc()
print("myfunc name:{}".format(myfunc.__name__))
执行结果如下:
spend time:2.00
func_name: myfunc
myfunc name:retFunc
这里我们通过上面提到的内包完成一个简单的装饰器,这里执行:
@decoTime
def myfunc():
等价于:
decoTime(myfunc)
所以会执行retFunc
函数的内容。
functools.wraps
另外值得注意的是,这里因为我们返回了一个retFunct
函数对象,因此myfunc
的__name__属性也被修改为了retFunc
。可是我们想保留__name__属性,那么就可以使用functools.wraps():
from time import time, sleep
from functools import wraps
def decoTime(func):
@wraps(func)
def retFunc():
start = time()
func()
now = time()
print("spend time:{:.2f}".format(now-start))
print("func_name: {}".format(func.__name__))
return retFunc
@decoTime
def myfunc():
sleep(2)
myfunc()
print("myfunc name:{}".format(myfunc.__name__))
使用wraps
之后的执行结果:
spend time:2.00
func_name: myfunc
myfunc name:myfunc
带参数的装饰器
装饰器也是能带参数滴:
from time import time, sleep
from functools import wraps
def decoTime(flag):
if flag=="normal":
def _decoTime(func):
@wraps(func)
def retFunc(*args, **kw):
start = time()
func(*args, **kw)
now = time()
print("spend time:{:.2f}".format(now-start))
print("func_name: {}".format(func.__name__))
return retFunc
return _decoTime
elif flag=="double":
def _decoTime(func):
@wraps(func)
def retFunc(*args, **kw):
start = time()
func(*args, **kw)
func(*args, **kw)
now = time()
print("spend time:{:.2f}".format(now-start))
print("func_name: {}".format(func.__name__))
return retFunc
return _decoTime
@decoTime("normal")
def myfunc1():
sleep(2)
@decoTime("double")
def myfunc2():
sleep(2)
myfunc1()
myfunc2()
执行效果:
spend time:2.00
func_name: myfunc1
spend time:4.01
func_name: myfunc2
说实话,这种带参数的装饰器相当ugly,三层嵌套。
singledispatch
singledispatch是用来实现单分派的装饰器,一个比较实用的装饰器,用来实现类似C++中的重载功能或者说解决函数中if-else过多的问题,简单通过下面的code可以说明:
from functools import singledispatch
@singledispatch
def fun(arg, verbose=False):
print(arg)
@fun.register(int)
def _(arg, verbose=False):
print(type(arg))
print(arg)
@fun.register(list)
def _(arg, verbose=False):
print(type(arg))
for i, elem in enumerate(arg):
print(i, elem)
fun(123)
fun(['a', 'b', 'c'])
执行结果:
<class 'int'>
123
<class 'list'>
0 a
1 b
2 c
这里分别为fun注册了两个匿名函数,根据入参的类型,执行不同的函数。
lru_cathe
装饰器lru_cathe是使用lru缓存算法(底层采用key-value保存结果)为函数结果保存结果,这样重复调用函数时,会使用之前缓存的结果,而不会重复计算:
from functools import lru_cache
cnt1 = 0
def fib1(n):
global cnt1
cnt1 = cnt1 + 1
if n < 2:
return n
return fib1(n-1) + fib1(n-2)
cnt2 = 0
@lru_cache(maxsize=None)
def fib2(n):
global cnt2
cnt2 = cnt2 + 1
if n < 2:
return n
return fib2(n-1) + fib2(n-2)
fib1(5)
fib2(5)
print("cnt1 : %d" %cnt1)
print("cnt2 : %d" %cnt2)
执行结果:
cnt1 : 15
cnt2 : 6
其中maxsize
是用来指定lru
的最大缓存大小,默认是128,如果设置为None缓存大小为无穷大。当然官方API建议,当maxsize
大小为2的指数时效率最高。
小结
对于python,经常面试的时候都会问到装饰器相关的内容,其实也有大牛建议使用__call__来代替装饰器的使用,其实工具的使用在于场景,但是脑袋需要知道有这些工具才能有机会比较,不然...google呀!
网友评论