Python 装饰器

1.装饰器的使用

当你希望给一个函数前后永久性地增加新功能,又不想修改原代码时可以使用装饰器。[或者是希望对不同的函数前后添加一致的功能时]函数和新功能都可以变。

2.与闭包的区分

一个向外部函数传入函数引用(装饰器),一个传入变量值(闭包)。

3.装饰器工作原理

装饰器其实只是嵌套函数的一种应用。它们封装一个函数,并且用这样或者那样的方式来修改它的行为。内部函数体中,可以在调用指定函数前后添加需要执行的指令,最后在外部函数体的末尾返回内部函数名即可。
@:只是一个语法糖而已,只是生成一个被装饰的函数的一个简短方式。另外,嵌套函数内部定义的内部函数在嵌套函数体外是不能访问的:

def a_new_decorator(a_func):
    def wrap_The_Function():
        """Hey you! I'm wrap_The_Function."""
        print("I am doing some boring work before executing a_func()")
        a_func()
        print("I am doing some boring work after executing a_func()")
    return wrap_The_Function

def a_function_requiring_decoration():
    """Hey you! Decorate me!"""
    print("I am the function which needs some decoration to remove")
     
a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)  # 装饰 a_function_requiring_decoration 函数
a_function_requiring_decoration()
# outputs:I am doing some boring work before executing a_func()
#         I am the function which needs some decoration to remove my foul smell
#         I am doing some boring work after executing a_func()

--------------------------------------------------------------------------------------------------------------------------------
@a_new_decorator
def a_function_requiring_decoration():
    """Hey you! Decorate me!"""
    print("I am the function which needs some decoration to remove my foul smell")
 
a_function_requiring_decoration()
#outputs: I am doing some boring work before executing a_func()
#         I am the function which needs some decoration to remove my foul smell
#         I am doing some boring work after executing a_func()
# @a_new_decorator 其实就只是下面这条语句的简短说明:
a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration)

# 不能从外部访问嵌套函数内部定义的函数
wrap_The_Function()  # NameError: name 'wrap_The_Function' is not defined

从上面的内容,我们可以看到,虽然最后调用的时候函数虽然还是同一个函数名字 a_function_requiring_decoration,但是打印它的 __name__ 和 __doc__看看:

print(a_function_requiring_decoration.__name__, '|', a_function_requiring_decoration.__doc__)
# outputs: wrap_The_Function | Hey you! I'm wrap_The_Function.

会发现这里的函数被 wrap_The_Function替代了,它重写了函数的名字和注释文档。不过,Python 提供了一个简单的函数来解决它,那就是 functools.wraps 。

4.functools.wraps

functools.wraps 可以将原函数对象的指定属性复制给包装函数对象,默认有module、name、doc,或者通过参数选择。
@wraps 接受一个函数来进行装饰,并加入了复制函数名称、注释文档、参数列表等等的功能。这可以让我们在装饰器里面访问在装饰之前的函数的属性。

from functools import wraps
 
def a_new_decorator(a_func):
    @wraps(a_func)
    def wrap_The_Function():
        """Hey you! I'm wrap_The_Function."""
        print("I am doing some boring work before executing a_func()")
        a_func()
        print("I am doing some boring work after executing a_func()")
    return wrap_The_Function
 
@a_new_decorator
def a_function_requiring_decoration():
    """Hey you! Decorate me!"""
    print("I am the function which needs some decoration to remove my foul smell")
 
print(a_function_requiring_decoration.__name__, '|', a_function_requiring_decoration.__doc__)
# Output: a_function_requiring_decoration | Hey you! Decorate me!
5.蓝本规范
from functools import wraps

def decorator_name(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if not can_run:
            return "Function will not run"
        return f(*args, **kwargs)
    return decorated

@decorator_name
def func():
    return("Function is running")
 
can_run = True
print(func())
# Output: Function is running
 
can_run = False
print(func())
# Output: Function will not run
6.几种常见形式

装饰带参数的函数传参:1.装饰器的内部函数 wrapper 需要传参; 2.内部函数中调用的被装饰函数也需要传参。

# 打印被装饰函数调用的结果,并在调用前后给出提示信息
def tips(func):
    def wrapper(a,b):  # 传参
        print('start')
        func(a,b)  # 调用时也需要传参
        print('stop')
    return wrapper

@tips
def add(a,b):  # 参数 a,b
    print(a + b)
    
@tips
def sub(a,b):  # 参数 a,b
    print(a - b)
    
add(3,2)  # start | 5 | stop; 格式所限,三个输出之间没有换行, | 隔开
sub(5,3)  # start | 2 | stop
add(4,2)  # start | 6 | stop
sub(6,3)  # start | 3 | stop

带参数的装饰器:如有必要,可以通过 if-else 针对不同的函数进行不同的装饰。

def new_tips(argv):  # 外面再加一层接收装饰器的参数
    print('传递装饰器参数: ', argv)
    
	def tips(func):  # 这层还是接收被装饰函数的函数名
        print('传递函数名:', func.__name__)  # 注意一下 func.__name__ 的值
        
        def wrapper(a,b):
            print('start %s, %s' % (argv, func.__name__))  # 注意一下 func.__name__ 的值
            func(a,b)
            print('stop %s, %s' % (argv, func.__name__))
        return wrapper  
    return tips

@new_tips('add_module')
def add(a,b):  # 参数 a,b
    print(a + b)
    
# ~ @new_tips('sub_module')
# ~ def sub(a,b):  # 参数 a,b
    # ~ print(a - b)

# 1.在 @ 的基础上直接调用函数
add(5,4)
# outputs:	传递装饰器参数:  add_module
# 			传递函数名:     add
# 			start add_module, add
# 			9
# 			stop add_module, add

# 2.注释掉 add 函数前的 @, 直接这么调用:
fun = new_tips('add_module')(add)
fun(5,4)
# outputs:	传递装饰器参数:  add_module
# 			传递函数名:     add
# 			start add_module, add
# 			9
# 			stop add_module, add

# 3.未注释掉函数前面的 @,直接这么调用
fun = new_tips('add_module')(add)
fun(5,4)
# outputs:	传递装饰器参数:  add_module
# 			传递函数名:     add
# 			传递装饰器参数:  add_module
# 			传递函数名:     wrapper
# 			start add_module, wrapper
# 			start add_module, add
# 			9
# 			stop add_module, add
# 			stop add_module, wrapper

# 4.在 3 的基础上,再将 sub 函数部分的注释取消掉,分别执行以上两种调用方式的话:
add(5,4)
# outputs:	传递装饰器参数:  add_module
# 			传递函数名:     add
# 			传递装饰器参数:  sub_module
# 			传递函数名:     sub
# 			start add_module, add
# 			9
# 			stop add_module, add

fun = new_tips('add_module')(add)
fun(5,4)
# outputs:	传递装饰器参数:  add_module
# 			传递函数名:     add
# 			传递装饰器参数:  sub_module
# 			传递函数名:     sub
# 			传递装饰器参数:  add_module
# 			传递函数名:     wrapper
# 			start add_module, wrapper
# 			start add_module, add
# 			9
# 			stop add_module, add
# 			stop add_module, wrapper

从上面可以看到:一到 @new_tips 的时候,就会去一步步执行函数 new_tips,直到返回函数名索引 wrapper,最后可直接调用被装饰函数 add 获得装饰后的输出。

多个装饰器叠加:以两个装饰器叠加为例,其顺序如下:显然,前面函数定义的部分仅仅是定义;到两个 @decorator 处时,会由里到外的一步步执行 decorator 函数的准备部分(暂且叫这个吧。。就是下面的 wrapper 函数外面的部分)。因为准备部分返回函数名索引,所以它接下来就会去执行下一个装饰器函数的准备部分。再然后经过 test 函数定义部分,准备完毕,开始调用。调用的时候,也许是因为之前返回的函数名索引是外层的在后面,现在开始调用的时候,外层的就会先调用。调用的大致顺序类似于这个模型:(out2前out1前 test )out2后 )out2后,左括号代表被装饰函数前面的装饰,右括号代表被装饰函数后面的装饰。

def decorator1(func):
    print('--out11--')
    
    def wrapper1(*args, **kwargs):
        print("--in11--")
        ret = func(*args, **kwargs)
        print("--in12--")
        return ret
    print("--out12--")
    return wrapper1
 
def decorator2(func):
    print('--out21--')
    
    def wrapper2(*args, **kwargs):
        print("--in21--")
        ret = func(*args, **kwargs)
        print("--in22--")
        return ret
    print("--out22--")
    return wrapper2


@decorator2
@decorator1
def test():
    print("--test run--")
    return 1 * 2

print("准备完毕,下面开始调用:")
test()
# outputs:	--out11--
# 			--out12--
# 			--out21--
# 			--out12--
#           准备完毕,下面开始调用:
# 			--in21--
# 			--in11--
# 			--test run--
# 			--in12--
# 			--in22--
# 			Out[11]: 2  # 这是 IPython 中才会显示出运算结果的, py 文件中是不会打印出来的, 因为它没有打印啊!

类装饰器:当应用的某些部分还比较脆弱时,异常也许是需要更紧急关注的事情。比方说有时你只想打日志到一个文件。而有时你想把引起你注意的问题发送到一个email,同时也保留日志,留个记录。这是一个使用继承的场景,但目前为止我们只看到过用来构建装饰器的函数。不过,类也可以用来构建装饰器。

# 以下文的日志场景为例
from functools import wraps

class Logit(object):  # __init__函数内接受装饰器参数, __call__函数内实现具体装饰器结构即可
    def __init__(self, logfile='out.log'):
        self.logfile = logfile
        
    def __call__(self, func):
        @wraps(func)
        def wrapped_function(*args, **kwargs):
            log_string = func.__name__ + 'was called'
            
            with open(self.logfile, 'a') as opened_file:  # save to the named file
                opened_file.write(log_string + '\n')
                
            self.notify()  # send a message
            return func(*args, **kwargs)
        return wrapped_function
    
    def notify(self):
        # Logit 只打日志, 不做别的
        pass
    
@Logit()  # Logit() 生成一个实例; 类中实现 __call__ 方法, 可以使实例如普通函数一样调用
def my_func1():
    pass


class EmailLogit(Logit):  # 继承 Logit
    def __init__(self, email='admin@myproject.com', *args, **kwargs):
        self.email = email
        super(EmailLogit, self).__init__(*args, **kwargs)
        
    def notify(self):
        # send a email to self.eamil
        # 这里没有写了
        pass

从现在开始,@email_logit() 将会和 @logit() 产生同样的效果,但是在打日志的基础上,还会多发送一封邮件给管理员。

7.不要滥用装饰器

目前只是看到这样一篇公众号文章,暂时体会不深,留待后续。装饰器的 @ ,不要再乱用了

8.使用场景
  • 日志

    # 指定一个用于输出的日志文件
    from functools import wraps
     
    def logit(logfile='out.log'):
        def logging_decorator(func):
            @wraps(func)
            def wrapped_function(*args, **kwargs):
                log_string = func.__name__ + " was called"
                
                with open(logfile, 'a') as opened_file:  # 打开 logfile, 并写入内容
                    opened_file.write(log_string + '\n')  # 现在将日志打到指定的logfile
                return func(*args, **kwargs)
            return wrapped_function
        return logging_decorator
     
    @logit()
    def myfunc1():
        pass
     
    myfunc1()
    # Output: myfunc1 was called
    # 现在一个叫做 out.log 的文件出现了, 里面的内容就是上面的字符串
     
    @logit(logfile='func2.log')
    def myfunc2():
        pass
     
    myfunc2()
    # Output: myfunc2 was called
    # 现在一个叫做 func2.log 的文件出现了,里面的内容就是上面的字符串
    
  • 权限校验等场景

    # 检查某个人是否被授权去使用一个web应用的端点(endpoint)
    from functools import wraps
     
    def requires_auth(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            auth = request.authorization
            if not auth or not check_auth(auth.username, auth.password):
                authenticate()
            return f(*args, **kwargs)
        return decorated
    
  • 函数缓存

    函数缓存允许我们将一个函数对于给定参数的返回值缓存起来。当一个 I/O 密集的函数被频繁使用相同的参数调用的时候,函数缓存可以节约时间。其实是将函数的输入和返回值缓存,下次再调用此函数时,若有相同的输入,那么返回值就可直接从缓存中提取,函数体本身无需再执行,用一点内存,加快了速度。
    在 Python 3.2 版本以前只能自定义实现。在 Python 3.2 以后版本,有个 lru_cache 的装饰器,允许我们将一个函数的返回值快速地缓存或取消缓存。

    # 实现一个佩波纳契计算器
    from functools import lru_cache
    
    @lru_cache(maxsize=32, typed=False)
    def fibs(n):
        if n < 2:
            return n
        return fibs(n-1) + fibs(n-2)
    
    print([fibs(n) for n in range(10)])  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
    

    maxsize: 最多缓存的次数,超过这个值之后,旧的结果就会被释放,然后将新的计算结果进行缓存,其值应设为 2 的幂,默认是128;若为None, 则无限制;
    typed 默认为 False;为 True 时:则不同参数类型的调用将分别缓存, 如 f(3), f(3.0).
    被 lru_cache 装饰的函数会有 cache_clear 和 cache_info 两个方法,分别用于清除缓存和查看缓存信息, 我们可以轻松地对返回值清空缓存:

    fibs.cache_info()  # CacheInfo(hits=16, misses=10, maxsize=32, currsize=10)
    fibs.cache_clear()
    fibs.cache_info()  # CacheInfo(hits=0, misses=0, maxsize=32, currsize=0) 参数尚未解识
    
  • 执行函数后清理功能

  • 执行函数前预备处理

  • 函数执行时间统计

参考资料:《Python 进阶》

上一篇:学习笔记(01):算法&数据结构-topk问题


下一篇:模块的制作与测试1