python高级部分之装饰器详解

时间:2020-8-24 作者:admin


python中的装饰器是一个非常重要的知识点,那在介绍装饰器之前大家先要了解闭包的概念,这样方便大家后面理解装饰器是什么?
那么先来介绍闭包的概念吧

一、什么是闭包

1.什么是闭包
• 在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生
闭包。
• 闭包可以用来在一个函数与一组“私有”变量之间创建关联关系。
• 在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。

2.形成闭包的条件

• 必须有一个内嵌函数(函数里定义的函数)——这对应函数之间的嵌套
• 内嵌函数必须引用一个定义在闭合范围内(外部函数里)的变量——内部函数引用外部变量
• 外部函数必须返回内嵌函数——必须返回那个内部函数

• 闭包的概念很简单:一个可以引用在函数闭合范围内变量的函数。即”内部函数”,只有那个内部函
数才有所谓的__closure__属性。

下面给个简单的例子给大家看下,就比较容易上面的话了。

def outer(x):
    a = 10
    def inner():
        print(x+a)
    return inner
d = outer(10)
d()
print(dir(d))
print(d.__closure__)
20
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
(<cell at 0x000001ACB1022370: int object at 0x00007FFC7B0E17C0>, <cell at 0x000001ACB1090190: int object at 0x00007FFC7B0E17C0>)

inner函数是outer函数的内嵌函数,在inner函数体中引用了外部函数的变量a,a的值为10,外部函数传的参数也为10,所以函数的输出结果为20。用dir看一下outer函数的属性会发现有一个__closure__属性,并且我在底下也输出了这个属性存放的空间在哪。

3.下面是大家需要注意的闭包的点

• 形成闭包之后,闭包函数会获得一个非空的__closure__属性(对比我们最后的函数test,test是一
个不具备闭包的函数,它的__closure__属性是None),这个属性是一个元组。
• 元组里面的对象为cell对象,而访问cell对象的cell_contents属性则可以得到闭包变量的当前值
(即上一次调用之后的值)。
• 而随着闭包的继续调用,变量会再次更新。
• 所以可见,一旦形成闭包之后,python确实会将__closure__和闭包函数绑定作为储存闭包变量的
场所。

4.闭包的坑

下面的代码我是想返回 0,1,4的,但是由于闭包函数有一些坑踩了才知道,下面的注释中讲的比较清楚,我也就不赘述了,大家以后注意点就好了。

#闭包的坑
def outer():
    fs = []
    for i in range(3):
        def inner():
            return i*i
        fs.append(inner)   #inner代表inner()函数的引用,即函数的内存地址
    return fs  #这里面其实只是三个inner的引用,并没有执行return,所以里面是没有值的,到后面才执行的inner()函数
fs1,fs2,fs3= outer()    #解包,fs里面有三个值,就要用三个引用来对应她
#fs1,fs2,fs3此处还未执行,outer执行完之后i变量值变为2

print(fs1())   #此处才执行inner函数,此时i的值为2
print(fs2())
print(fs3())
4
4
4

5.闭包是必要的吗?
可能大家会关心闭包是必要的吗?

• 闭包不是必须的。
• 没了闭包,python的功能一点不会被影响
• 有了闭包,只是提供给你一种额外的解决方案。

二、什么是装饰器

1.什么是装饰器

• 装饰器是这样一种设计模式:如果一个类(函数)希望添加其他类(函数)的一些功能,而不希望
通过继承或是直接修改源代码实现,那么可以使用装饰器模式。

• 简单来说Python中的装饰器就是指某些函数或其他可调用对象,以函数或类作为可选输入参数,
然后返回函数或类的形式。通过这个在Python2.6版本中被新加入的特性可以用来实现装饰器设计
模式。

• 装饰器就是一个可以接受调用也可以返回调用的函数,该函数接受被装饰的函数作为其位置参数。
装饰器通过使用该参数来执行某些操作,然后返回原始参数或一些其他的调用

• 函数也是对象,也可以当做参数传递

其实简单来说装饰器就是一个闭包函数。至于怎么编写和使用的,下面会介绍,这里只是介绍一下什么是装饰器。

2.装饰器的作用
你可以考虑在装饰器中置入通用功能的代码来降低程序复杂度。例如,可以用装饰器来:

• 引入日志
• 增加计时逻辑来检测性能
• 给函数加入事务的能力
• 权限控制
等等

三、编写和使用装饰器

前面说了这么多,现在来给大家介绍装饰器怎么编写和如何使用的。

下面给大家演示一个装饰器的例子

1.用装饰器实现权限控制

• 定义一个全局变量:username
• 定义add函数,实现两个数相加
• 实现login_required装饰器,如果username值为root,提示”欢迎”
并计算结果,否则”没有权限”

#用装饰器实现权限控制
def login_required(func):
    global username
    username = "root"
    def power(x,y):
        if username == "root":
            print("欢迎")
            ret = func(x,y)
            return ret
        else:
            print("没有权限")

    return power
@login_required
def add(x,y):
    b = x+y
    print(b)

add(10,20)
欢迎
30

上面的login_required函数其实就是一个闭包函数,传的参数是一个函数,所以这个闭包函数就是一个装饰器,参数func实际上就是下面装饰的add函数,闭包函数体内实现了username的判断,是root的话就调用add函数,不是root的话就输出没有权限,最后是要返回内嵌函数名的,就是return power这里是不可丢的。

简单来说就是,add函数其实就只是完成数字的加法而已,但是因为被装饰器装饰了,所以有的额外的功能,使用装饰器就是用@加上你编写的装饰器名,@装饰器名下方的函数就是你装饰的函数,这个函数有本身不具备的额外功能,这个功能是装饰器带来的,也就是你编写的这个闭包函数体内实现的功能额外的加在了你装饰的这个函数上,这就是装饰器的作用。

2.上面我只是使用了一个装饰器,那么能不能编写多个装饰器和使用多个装饰器呢

答案是显然的。

就上面我举的例子来说,在实际应用的场景中,当我们采用上面的方式写了两个装饰方法比如先验证有没有登录@login_required , 再验证权限够不够时 @permision_allowed,这样使用两个装饰器来完成上面的功能是不是也可以呢。

import re
name = input("请输入用户名:")
passwd = input("请输入你的密码:")
#正则来检测用户名和密码是否符合标准
list1 = re.findall("^\w{4,}$",name)    #用户名最少四位而且是只有字母数字下划线组成并且以它们为开头和结尾
list2 = re.findall("^.*\w{8,}[^\w]+.*$",passwd)   #密码至少包含一个特殊字符,字母数字也包含且长度最少9位,不能以数字字母下划线结尾


def login_required(func):
    def _check(*args):
        if list1 != [] and list2 != []:   #判断用户名和密码是否符合标准
            print("登陆成功")
            ret = func(*args)   #调用permission_allowed函数
            # print("登陆成功")
            return ret
        else:
            print("没有登陆成功")
    return _check

def permission_allowed(func):
    def power(*args):
        if name == "root":
            print("权限足够")
            ret = func(*args)   #调用add函数
            # print("########")
            return ret
        else:
            print("权限不够")
    return power

@login_required
@permission_allowed
def add(x,y):
    print("使用加法运算")
    return x + y
sum = add(10,20)
print("加法运算的最终结果为{}".format(sum))
请输入用户名:root
请输入你的密码:#dgsj4254EWr%
登陆成功
权限足够
使用加法运算
加法运算的最终结果为30

应用多个装饰器的时候,封装时是至下而上的,执行时是至上而下的。

从上面的例子可以看出来,编写了两个装饰器,先用了login_required这个编写好的装饰器,下面再用permission_allowed这个编写好的装饰器,格式为下面的格式

@login_required
@permission_allowed

这样的话就是先使用login_required这个装饰器的功能,给add函数加上判断用户登录是否成功的功能,再使用permission_allowed装饰器,如果用户登陆成功,给add函数加上用户是否为root,如果是执行add函数的加法功能,如果不是,输出不是root用户。这样就是多个装饰器的编写和使用,注意执行顺序这是很关键的,上面的代码使用正则来判断用户登录是否成功的,关于正则表达式我也会专门写一篇博客来讲解正则表达式,毕竟正则也是一个很重要的知识点。

3.保留元数据

什么是元数据

元数据(Metadata),又称中介数据、中继数据,为描述数据的数据(data about data),主要是
描述数据属性(property)的信息
函数的重要的元信息比如名字、文档字符串、注解和参数签名等

装饰器后为什么元数据会丢失
因为return执行的,是经过调用封装后的函数

怎么保留元数据

利用@functools.wraps(fun),将一个函数的重要内容复制到另一个函数
温馨提醒:任何时候你定义装饰器的时候,都应该使用 functools 库中的 @wraps 装饰器来注解底层
包装函数。

import time
import functools

def cost(func):
    print("i am cost")

    #@functools.wraps(func)
    # 将func的元数据复制给_log
    #     #保留元数据
    def _cost():
        start = time.time()
        print("start........")
        ret = func()
        end = time.time()
        print("end.........")
        runtime = end - start
        print("cost:",runtime)
        return ret
    return _cost

@cost
def func1():
    print("i am func1")

# func1()
print(func1.__name__)  #如果没有@functools.wraps这个装饰器的话,这里返回的值应该是_log,有的话返回的就是func1自身
i am cost
_cost

Process finished with exit code 0

可以看到我将上面的@functools.wraps(func)注释了,输出的func1的名字不是func1本身自己的名字,而是闭包函数中的内嵌函数名_cost,这是因为没有保留元数据的原因。那我们看下利用@functools.wraps(fun)这个装饰器的情况

import time
import functools

def cost(func):
    print("i am cost")

    @functools.wraps(func)
    # 将func的元数据复制给_log
    #     #保留元数据
    def _cost():
        start = time.time()
        print("start........")
        ret = func()
        end = time.time()
        print("end.........")
        runtime = end - start
        print("cost:",runtime)
        return ret
    return _cost

@cost
def func1():
    print("i am func1")

# func1()
print(func1.__name__)  #如果没有@functools.wraps这个装饰器的话,这里返回的值应该是_log,有的话返回的就是func1自身
i am cost
func1

Process finished with exit code 0

可以看到使用了@functools.wraps(func)后,输出的名字是func1本身自己的名字了,这个 functools 库中的 @wraps 装饰器来注解底层包装函数的效果很明显。

内置的装饰器
Python还提供了类装饰器与@staticmethod,@classmethod,@property和这三个在面向对象编
程中常用的装饰器

• staticmethod:把类中定义的实例方法变成静态方法
• classmethod:把类中定义的实例方法变成类方法
• property:把类中定义的实例方法变成类属性。

这是在面向对象里会使用的装饰器,面向对象这个知识点我也会写,后面抽空会写出来,新人写手不容易,大家可以关注一下。

四、装饰器的应用

首先大家考虑的问题应该是为什么要使用装饰器,可不可以不使用呢?还有就是在哪里使用呢?

为什么使用装饰器

• 模块化且清晰明确
• 代码重用
• 装饰器是显式的,增强可读性

在何处使用装饰器
Python标准库中包括很多包含装饰器的模块,并且很多常用工具和框架利用它们实现常功能,如:

• 要使用一个类上的方法不需要这个类的实例(@classmethod或@staticmethod)
• mock模块(单元测试python3.3后加入标准库-@mock.patch或@mock.patch.object)
• Django(@login_required, @permission_reqired)
• Flask(@app.route) 充当url与函数之间的注册表
• Celery(@task) 返回Task类实例
• 插入日志、性能测试、事务处理、缓存、权限校验等场景
• 记录结果、增加计时逻辑来检测性能等

下面举一个装饰器的例子:
日志文件
日志记录
日志 是一种可以追踪某些软件运行时所发生的时间的方法
作用: 通过记录和分析日志可以了解一下系统运行情况是否正常,快速定位问题。

既然说到日志了,就得说日志的等级
日志等级
根据一个事件的重要性,划分几个详细等级
不同的程序所定义的日志等级可能会有所差异,分详细点可以包含如下等级:
debug
info
notice
warning
error
critical
alert
emergency

python中是logging模块记录日志
python logging
logging日志等级:
debug #最详细的日志信息
info #详细程度仅次于debug,记录关键节点信息
warning #当某些不期望的事情发生的时候记录的信息,但是程序是正常运行的
error #一个更严重的问题引起了某些功能不能正常运行是记录信息
critical #发生严重错误,程序不能正常运行时记录信息

使用logging日志系统的的四大组件:

日志器 –》Logger #提供了应用程序可使用的接口
处理器 –> Handler #将logger创建的日志记录发送到合适的目的输出
过滤器 –>Filter #更细力度的控制工具来决定输出哪条日志,丢弃哪条日志
格式器 –>Formatter # 决定日志最终的输出格式

话不多说,直接例子:

# log装饰器
# 记录执行了什么函数到日志文件中
import logging
def log(func):
    def inner(x):
        logger = logging.getLogger()   #获取logger对象
        fh = logging.FileHandler("test1.log", encoding="utf-8")    #创建一个日志输出到文件的Handler对象
        ch = logging.StreamHandler()    #创建一个输出控制台的Handler
        formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")   #创建一个formatter日志格式
        fh.setFormatter(formatter)    #绑定formatter到handler上
        logger.addHandler(fh)   #绑定 handler 到 logger对象上
        logger.addHandler(ch)    #绑定 handler 到 logger对象上
        logger.setLevel(logging.DEBUG)    #设置日志等级为DEBUG
        ret = func(x)
        logger.debug(f"执行了函数:{func.__name__}")
        return ret
    return inner

@log
def logg(x):
    print(x)
logg(10)
执行了函数:logg
10

Process finished with exit code 0

test1.log里面的内容就多加了一行:
2020-08-23 21:23:36,435 – root – DEBUG – 执行了函数:logg

因为我代码中是既输出到控制台上,也往文件里面写入,所以两个里面都会有内容。

上面就是一个装饰器的应用,可以用来记录日志,log装饰器可以给包装函数额外增加记录日志的功能,装饰器应用比较多,这里就举一个日志的例子了。

五、装饰器的更多用法

1.装饰器带参数

装饰器是可以带参数的,举个例子先

#装饰器带参数
import functools
def deco(arg):
    def cost(func):
        @functools.wraps(func)
        def _cost(*args,**kwargs):
            print("start.......")
            func(*args,**kwargs)
            print(f"args is ... {arg}")
            print("end.........")
        return _cost
    return cost

@deco(1)   #deco(1)(func1)()—>_cost(),这里的func1()相当于_cost(),也就是调用_cost(),里面的func()才是执行func1()
def func1():
    print("i am func1")

func1()
start.......
i am func1
args is ... 1
end.........

Process finished with exit code 0

这个是装饰器的扩展,就是装饰器是可以带参数的,大家了解就行,这个用法不多。

2.基于类实现装饰器

上面我们都是用函数来编写的装饰器,那么装饰器是只能用函数来编写吗?不是的,用类也可以编写装饰器。那么怎么编写呢?

#基于类实现装饰器
#装饰器需要一个callable对象,并返回一个callable对象,用类去实现也是可以的
#可以让类的构造函数接受一个函数,然后__call__函数处理并返回一个函数
import time
class A():
    def __init__(self,func):
        self.func = func

    def __call__(self,*args,**kwargs):
        print("this is A.__call__")
        start = time.time()
        ret = self.func(*args,**kwargs)
        end = time.time()
        print(end - start)
        return ret

@A
def func1():
    print("i am func1")
    time.sleep(1)

func1()
this is A.__call__
i am func1
1.0009000301361084

Process finished with exit code 0

具体用类实现装饰器的原理是:

装饰器需要一个callable对象,并返回一个callable对象,用类去实现也是可以的
可以让类的构造函数接受一个函数,然后__call__函数处理并返回一个函数

其实说到头来还是闭包的概念,所以说装饰器实际上是什么?你可以回答说就是一个闭包函数,可以给包装的函数添加额外的功能。

以上就是装饰器的概念和编写、使用等的详解,希望大家看了会对大家有所帮助,本人是一个新人写手,谢谢大家的支持,喜欢我的文章的话可以点个关注,后续我还会写和python知识相关的博客的,感谢观看!!!!!

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。