python进阶——7. 装饰器

7.1 创建装饰器

与java中的装饰器模式类似,其作用就是将一些多余的、能够重复使用的代码抽离出来,然后通过python的语法糖@作用在方法头部,能够起到对方法增进、装饰的作用。类似的像统计功能,log日志功能等,都可以作为装饰器的模式供其他方法使用。

下面的实例是实现一个数列,类似1, 1, 2, 3, 5, 8,13... ...一个数等于前两个数之和,能够查询具体的第n个数。

def fibonacci(n):
    if n <= 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(5))

实现逻辑很简单就是重复递归,最后一直调用到fibonacci(0), fibonacci(1)的值时才停止。但是仔细分析可知,在此过程中会造成一些不必要的计算,例如输出的n为5,下一步计算的是fibonacci(4) + fibonacci(3),再下一步(fibonacci(3) + fibonacci(2)) + (fibonacci(2) + fibonacci(1)),可以看出fibonacci(3)重复计算了。这种在数目特别大的情况下会导致运行速度很慢。

如果需要求出第50项的数,就需要去掉一些重复的计算,可以通过添加缓存机制来解决。

def fibonacci(n, cache=None):
    if cache is None:
        cache = {}
    if n in cache:
        return cache[n]
    if n <= 1:
        return 1
    cache[n] = fibonacci(n - 1, cache) + fibonacci(n - 2, cache)
    return cache[n]

print(fibonacci(50))

添加参数cache,当cache为空时,创建一个空的字典,将递归运行的结果存储在cache中,当之后递归遇到相同的结果时直接从缓存字典中取出即可,这样实现之后,大数字的运行速度会有明显提升。

类似的很多其他算法也可能会遇到此类问题,需要建立一个缓存机制进行处理,所以有必要将缓存机制封装出来供其他方法调用。下面是创建的memo方法,此方法接收的是函数闭包对象,然后创建内函数并在外层函数返回此内函数对象。在内函数中接收传来的参数,进行对应处理操作。

在调用memo方法时传入方法本身,并且生成一个新的方法对象,然后在这个新的方法对象中传入想要的参数值。

def memo(func):
    cache = {}
    def wrap(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrap


def fibonacci(n):
    if n <= 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

fibonacci = memo(fibonacci)
print(fibonacci(50))

在python中对装饰器有个实用的语法糖,就是在使用装饰器的方法上添加@加上装饰器的方法名即可。

def memo(func):
    cache = {}
    def wrap(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrap

@memo
def fibonacci(n):
    if n <= 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)

# fibonacci = memo(fibonacci)
print(fibonacci(50))

7.2 为装饰器的函数保留元数据

在python中函数也属于对象,函数的元数据相当于对象的属性。下面看以下常用的函数属性。

def f(a, b=10):
    ''' f test method

    :param a:
    :param b:
    :return:
    '''
    d = 10
    print("f")

print(f.__doc__)
print(f.__defaults__)
print(f.__name__)
print(f.__module__)

 f test method

    :param a:
    :param b:
    :return:
    
(10,)
f
__main__

doc函数的文档
defaults函数的默认参数
name函数名
module函数所属的模块

另外,对于函数内部的闭包来说,可以通过函数的属性访问闭包。

def t():
    a = 2
    return lambda k: a ** k

g = t()
print(g.__closure__[0].cell_contents)

2

在t方法中有一个a字段,函数的返回值是一个lambda函数。当调用f方法时,a属性在闭包中,g变量可以通过closure访问函数闭包。

当使用装饰器时,装饰器函数可能会影响到调用的函数属性

def my_decortor(func):
    def wrapper(*args, **kwargs):
        """wrapper func """
        print("in wrapper")
        func(*args, **kwargs)
    return wrapper

@my_decortor
def example():
    '''example func'''
    print("example func")

print(example.__doc__)
print(example.__name__)

wrapper func 
wrapper

因为在函数调用装饰器函数时,其函数对象已经发生了改变,变为了装饰器函数对象,所以调用example的函数元数据编成的是my_decortor的元数据了。

如果想保留原来调用函数的元数据,可以使用标准库functools中的wraps装饰内部包裹函数,可以将原来的函数属性更新到包裹函数中。

from functools import wraps, WRAPPER_ASSIGNMENTS, WRAPPER_UPDATES, update_wrapper

def my_decortor(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """wrapper func """
        print("in wrapper")
        # update_wrapper(wrapper, func)
        func(*args, **kwargs)
    return wrapper

@my_decortor
def example():
    '''example func'''
    print("example func")

print(example.__doc__)
print(example.__name__)

example func
example

当给包裹函数加上装饰器@wraps,并传入原函数对象即可更新元数据到包裹函数上。其原理是调用update_wrapper(wrapper, func)方法,只不过使用装饰器调用更加简便。上面调用都是使用的默认参数,在装饰器@wraps的传参中可以传入其他的属性,默认参数具体为('module', 'name', 'qualname', 'doc', 'annotations')。

7.3 定义有参数的装饰器

带参数的装饰器,也就是根据参数定制化一个装饰器,可以看做生产装饰器的工厂。

例如创建一个装饰器,其具有判断函数的参数类型是否为所需类型。具体的思路是在普通的装饰器 外层再套上一个工厂方法来接收给装饰器传入的参数,然后通过方法签名信息inspect库中的signature来获取参数的信息,然后判断参数类型是否为装饰器定义类型,如果不是抛出异常即可。

from inspect import signature


def type_assert(*ty_args, **ty_kargs):
    def decorator(func):
        sig = signature(func)
        btypes = sig.bind_partial(*ty_args, **ty_kargs).arguments

        def wrapper(*args, **kwargs):
            for name, obj in sig.bind(*args, **kwargs).arguments.items():
                if name in btypes:
                    if not isinstance(obj, btypes[name]):
                        raise TypeError('"%s" must be "%s"' % (name, btypes[name]))

            return func(*args, **kwargs)

        return wrapper

    return decorator


@type_assert(int, str, list)
def f(a, b, c):
    print(a, b, c)

f(1, 1, 1)

signature主要是为了获取方法的相关信息,类似下面,能够获取出参数的名称、类型和默认值相关,并且通过bind方法绑定参数信息。


def test(a, b, c=1): pass

sig = signature(test)
print(sig.parameters)
print(sig.parameters['a'].name)
print(sig.parameters['a'].kind)
print(sig.parameters['c'].default)
bargs = sig.bind(str, int, int)
print(bargs.arguments['a'])

7.4 属性可修改的函数装饰器

在某些情况下,我们需要动态改函数装饰器的参数、属性值。

下面看一个实例,首先实现一个能够计算函数运行时长的装饰器,并且传入timeout参数,如果函数执行的时长超过了timeout规定,就打印出相关信息。

from functools import wraps
import time
import logging
from random import randint


def warn(timeout):
    def decorator(func):
        def wrapper(*args, **kwargs):
            start = time.time()
            res = func(*args, **kwargs)
            used = time.time() - start
            if used > timeout:
                msg = '"%s": %s > %s ' % (func.__name__, used, timeout)
                logging.warning(msg)
            return res
        return wrapper
    return decorator


@warn(1.5)
def test():
    print('In test')
    while randint(0, 1):
        time.sleep(0.5)


for x in range(1, 31):
    test()

在test方法中使用warn装饰器进行修饰,并且传入timeout为1.5的参数,之后随机地睡眠0.5秒,启动执行30次。可以看到随机打印出了log,WARNING:root:"test": 2.059203624725342 > 1.5

接下来,上限在外层动态修改timeout的值。

from functools import wraps
import time
import logging
from random import randint


def warn(timeout):
    def decorator(func):
        def wrapper(*args, **kwargs):
            start = time.time()
            res = func(*args, **kwargs)
            used = time.time() - start
            if used > timeout:
                msg = '"%s": %s > %s ' % (func.__name__, used, timeout)
                logging.warning(msg)
            return res

        def set_timeout(k):
            nonlocal timeout
            timeout = k
        wrapper.set_timeout = set_timeout
        return wrapper

    return decorator


@warn(1.5)
def test():
    print('In test')
    while randint(0, 1):
        time.sleep(0.5)


# for x in range(1, 31):
#     test()

test.set_timeout(1)

for x in range(1, 31):
    test()

想要动态修改装饰器的参数,就需要对包裹函数增加属性,此属性可以是内函数,然后在内函数中对传入的装饰器参数进行进一步操作。需要注意的是,timeout是闭包内的数据,新建的内函数是不能直接访问到此参数,在python3中提供了nonlocal 修饰符可以访问到闭包内的数据。

执行结果,随机打印出WARNING:root:"test": 1.0000569820404053 > 1

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,600评论 18 139
  • Python进阶框架 希望大家喜欢,点赞哦首先感谢廖雪峰老师对于该课程的讲解 一、函数式编程 1.1 函数式编程简...
    Gaolex阅读 5,489评论 6 53
  • 呵呵!作为一名教python的老师,我发现学生们基本上一开始很难搞定python的装饰器,也许因为装饰器确实很难懂...
    TypingQuietly阅读 19,518评论 26 186
  • 要点: 函数式编程:注意不是“函数编程”,多了一个“式” 模块:如何使用模块 面向对象编程:面向对象的概念、属性、...
    victorsungo阅读 1,468评论 0 6
  • 夜里窗外火车匆匆驶过 咣当咣当地很沉重 不知是载着归来的游子 还是远走的儿郎 火车在时间线上跑着 呼啸着 无数人的...
    大棚盖浇饭阅读 181评论 0 0