Python 内包和装饰器

2018-02-18  本文已影响0人  大雄good

内包和装饰器

PythonJavaScript(暂时只研究过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_listcal_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呀!

上一篇 下一篇

猜你喜欢

热点阅读