ITPub博客

首页 > 数据库 > 数据库开发技术 > python面向对象编程篇(19):闭包和装饰器

python面向对象编程篇(19):闭包和装饰器

原创 数据库开发技术 作者:程序员小钟同学 时间:2021-03-22 18:30:00 0 删除 编辑

在之前flask脚手架系列里提到关于装饰器的概念,再这里延伸扩展装饰器的一些知识点。

1 闭包

在了解装饰器之前,其实我们有必要先了解一下闭包这个东西,其实闭包是装饰器的基础。也可以说是:装饰器其实是必要的一种实现形式。

1、闭包的定义

闭包 专业术语来说是指在方法内引用方法外定义的【非全局变量】,且最外层方法返回的是新方法内的方法。

几个关键词典:

  • 方法内
  • 在定义一个新的方法引用方法外层
  • 引用的原来方法内的一个【非全局变量】
  • 返回新方法的地址

示例如:


# 最外层的方法函数
def outerfun():
    # 非全局变量的对象
    name = '小钟同学'
    # 内层新的方法
    def innerfun():
        return name
    # 返回内层的方法地址
    return innerfun

xiaozhong = outerfun()
print(xiaozhong)
print('对象化调用',xiaozhong())

输出结果为:

<function outerfun.<locals>.innerfun at 0x000001C79C403D08>
对象化调用 小钟同学

我们知道一个关于函数的内部变量的作用域的问题,按理来说,

  • name是外部函数的作用域的变量,应该仅限于outerfun内的函数内的作用域内的调用,但是现在也可以被内嵌的函数锁引用。

换而言之:一个能记住嵌套作用域变量值的函数,尽管作用域已经不存在 那么它就是一个闭包函数。

PS:一切皆对象的补充说明:一个python函数内部的执行过程:

  • 1、python解析器遇到def语句的时候,会在内存中生成一个函数对象,且使用这个对象考函数名作为一个变量来做函数>对象的引用,

  • 2、函数体内部的语句只有在函数的调用的时候才会被执行

  • 3、而函数调用结束了,就是函数返回时,其内部所有所生成的数据所有都会被销毁

  • 4、如果函数内还有一个内层函数,那么内存函数可以调用记忆外层函数中的变量

2、nonlocal在闭包中的作用

在闭包中如果涉及对我们的外层的变量进行新的内存分配的时候就会报错!

此时引入nonlocal关键词,把变量标记为一个自由变量,即使在函数中为变量赋予了新值,也会变成自由变量。

2、装饰器

2.1定义

装饰器其实是在闭包的基础上再传递了一个函数,覆盖了原来函数的执行入口,以至后续调用这个函数的时候,就可以额外附加一些功能,而不影响到内部原来的函数的修改。

所以我们可以理解为装饰器本质上就是其实也还是一个函数,它的返回值也是一个函数对象。

装饰器常用需求的场景有:

这些装饰器其实可以为我们的作为:

  • 日志统计模块
  • 性能测试
  • 事务处理
  • 认证鉴权
  • 信号发送
  • 异常捕获
  • 路由分发
  • 缓存处理
  • 权限校验

如我们的之前的学习到的Flask的几个装饰器函数:

 @app.errorhandler(Exception)
 @app.before_first_request
 @app.before_request
 @app.after_request
 @app.teardown_appcontext

如我们的自定义的装饰器:

import functools
from core.helper.http_helper import get_flask_heards_key
from core.helper import jwt_helper

from core.config import config as config_hepler
from app_youbao_admin.service.logic_sys_user import LogicSysUser
from app_youbao_admin.exceptions import ForbiddenException, InvalidTokenException, ExpiredTokenException
from app_youbao_admin.validators.forms import SysPermissionForm
from app_youbao_admin.api.logaction import add_sys_log_action


# 带参数且返回相关的值得装饰器
def check_required(token_key='X-Access-Token', nees_check_perms=None,perms_describes=None, nees_check_perms_lists=None,perms_describes__lists=None,is_log_operation=True, is_back_parameter=False):
    """
    :param token_key: 默认的头部的信息(提交的头部信息)
    :param nees_check_perms: 需要的权限标记
    :param is_log_operation: 是否需要记录日志
    :return:
    "
""

    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            # 头部提取的token信息进行校验
            token = get_flask_heards_key(token_key)
            # print('token',token)
            if not token:
                # 从get里面进行提取
                form = SysPermissionForm().validate_for_api()
                if not form.token.data:

                    # 如果是需要进行日志记录的话则, 添加相关的统计
                    if is_log_operation:
                        add_sys_log_action(log_type=1, log_content='用户登录失败,没token进行提交')

                    raise InvalidTokenException()
                token = form.token.data
            # 验证TOEKN
            isok, state, token_result = jwt_helper.verify_bearer_token_state(secret=config_hepler.get('token', 'TOKEN_SECRETKEY'), token=token)
            # print('token_result', token_result)
            if not isok and state == 1:

                # 如果是需要进行日志记录的话则, 添加相关的统计
                if is_log_operation:
                    add_sys_log_action(log_type=1, log_content='用户登录失败,用户名TOKEN错误!')

                raise InvalidTokenException()
            if not isok and state == 2:

                # 如果是需要进行日志记录的话则, 添加相关的统计
                if is_log_operation:
                    add_sys_log_action(log_type=1, log_content='用户登录失败,用户名TOKEN超过有效期!')

                raise ExpiredTokenException()

            # 验证用户有效性处理
            isok, isstatus = LogicSysUser.get_user_model_by_username(username=token_result.get('username'))
            # 用户状态(1-正常,2-冻结)
            if isok and isstatus.get('status') == 2:
                    # 如果是需要进行日志记录的话则, 添加相关的统计
                    if is_log_operation:
                        add_sys_log_action(log_type=1, log_content='用户登录失败,用户名:%s已冻结!' % (token_result.get('username')), username=token_result.get('username'), userid=token_result.get('userid'))
                    return ForbiddenException()

            # 验证对应的接口的权限信息
            if nees_check_perms:
                # 权限标记
                if token_result.get('permissions'):
                    permissions = token_result.get('permissions')
                    if nees_check_perms.strip() not in permissions:
                        # 无权限
                        # 如果是需要进行日志记录的话则, 添加相关的统计
                        if is_log_operation:
                            add_sys_log_action(log_type=2, log_content='操作失败,没%s相关的操作权限' % (nees_check_perms))
                        if perms_describes:
                            return ForbiddenException(code=200,api_code=403,success=False,message=perms_describes)
                        return ForbiddenException(code=200,api_code=403)
                else:
                    if perms_describes:
                        return ForbiddenException(code=200,api_code=403,message=perms_describes)
                    return ForbiddenException(code=200,api_code=403)


            # 返回携带的参数信息
            if is_back_parameter:
                return fn(token_result, *args, **kwargs)
            return fn(*args, **kwargs)

        return wrapper

    return decorator

2.2装饰器优点:

  • 更加清晰优雅的代码结构
  • 封装特定的功能代码,便于代码复用

2.3装饰器几种实现类型

2.3.1 普通类型装饰器

给普通的函数新增额外功能:


# 装饰器函数,参数 func 是被装饰的函数
def decorator(func):
    def wrapper(*args, **kw):
        print('在飞之前,我先给你加满油:'.format(func.__name__))
        # 真正执行的是这行。
        func(*args, **kw)
        print('飞完之后,我给你找个飞机场!')
    return wrapper

@decorator
def ceshi_decorator():
     print("我正在飞······")


ceshi_decorator()

输出结果:

在飞之前,我先给你加满油:
我正在飞······
飞完之后,我给你找个飞机场!

2.3.2 可以传入参数装饰器

#新增一个函数内嵌我们的普通装饰器,且返回我们的普通的装饰器
def my_decorator(type='飞机类型'):
    # 装饰器函数,参数 func 是被装饰的函数
    def decorator(func):
        def wrapper(*args, **kw):
            print("接收传入的参数:",type)
            print('在飞之前,我先给你加满油:'.format(func.__name__))
            # 真正执行的是这行。
            func(*args, **kw)
            print('飞完之后,我给你找个飞机场!')
        return wrapper
    return decorator

@my_decorator(type="民航飞机")
def ceshi_decorator():
     print("我正在飞······")


ceshi_decorator()

输出结果:

接收传入的参数: 民航飞机
在飞之前,我先给你加满油:
我正在飞······
飞完之后,我给你找个飞机场!

带参数的类型的装饰器,其实是再普通的函数的基础上又再添加外层函数。

2.3.3 类装饰器

类装饰器其实理由的我们的类变为可调用对象的性质来实现,意思就是接触于 call 和 __init__两个内置函数。

init :用于接收传入需要被装饰的函数对象call :实现具体的装饰逻辑处理,即返回调用这个传入的被装饰的函数对象之前做什么相关的操作的处理。

class FeijiDecorator():
    def __init__(self, func):
        # 接收被装饰的函数对象
        self.func = func

    def __call__(self, *args, **kwargs):
        # 再调用被装饰函数之前做一些相关的操作
        print('在飞之前,我先给你加满油:'.format(self.func.__name__))
        self.func(*args, **kwargs)
        print('飞完之后,我给你找个飞机场!')
        return



@FeijiDecorator
def ceshi_decorator():
     print("我正在飞······")

输出结果:

在飞之前,我先给你加满油:
我正在飞······
飞完之后,我给你找个飞机场!

2.3.4 类装饰器且可以传入参数

class FeijiDecorator():
    def __init__(self, type='飞机类型',chengyuantype = "成员类型"):
        # 接收被装饰的函数对象
        self.type = type
        self.chengyuantype = chengyuantype

    def __call__(self,func):
        # 再调用被装饰函数之前做一些相关的操作
        def wrapper(*args, **kwargs):
            print("接收传入的type参数:", self.type)
            print("接收传入的chengyuantype参数:", self.chengyuantype)
            print('在飞之前,我先给你加满油:'.format(func.__name__))
            func(*args, **kwargs)
            print('飞完之后,我给你找个飞机场!')
        return wrapper  # 返回函数



@FeijiDecorator(type="民航飞机",chengyuantype='活体类型')
def ceshi_decorator():
     print("我正在飞······")


ceshi_decorator()

多个装饰器装饰同一个函数的调用顺序

#!/usr/bin/evn python
# coding=utf-8




# 装饰器函数,参数 func 是被装饰的函数
def decorator(func):
    def wrapper(*args, **kw):
        print('1号装饰器-在飞之前,我先给你加满油:'.format(func.__name__))
        # 真正执行的是这行。
        func(*args, **kw)
        print('1号装饰器-飞完之后,我给你找个飞机场!')
    return wrapper

def decorator_2(func):
    def wrapper(*args, **kw):
        print('2号装饰器-在飞之前,我先给你加满油:'.format(func.__name__))
        # 真正执行的是这行。
        func(*args, **kw)
        print('2号装饰器-飞完之后,我给你找个飞机场!')
    return wrapper

@decorator
@decorator_2
def ceshi_decorator():
    print("我正在飞······")



ceshi_decorator()


输出结果为:

1号装饰器-在飞之前,我先给你加满油:
2号装饰器-在飞之前,我先给你加满油:
我正在飞······
2号装饰器-飞完之后,我给你找个飞机场!
1号装饰器-飞完之后,我给你找个飞机场!

总结:如果存在多个装饰器的情况,装饰器的执行的顺序的是,由上而下的执行,且遵循的之前我们的钩子函数的一样的洋葱模型!

装饰器的变异

上面几种装饰器的方式,内部会对我们的内嵌函数进行对象进行改造。如:

1、没有装饰之前普通装饰器:


# 装饰器函数,参数 func 是被装饰的函数
def decorator(func):
    def wrapper(*args, **kw):
        print('在飞之前,我先给你加满油:'.format(func.__name__))
        # 真正执行的是这行。
        func(*args, **kw)
        print('飞完之后,我给你找个飞机场!')
    return wrapper


def ceshi_decorator():
    print("我正在飞······")


print("没有被装饰之前,我们的函数的名称是:",ceshi_decorator.__name__)

输出结果为:

没有被装饰之前,我们的函数的名称是:ceshi_decorator

2、被装饰之后

# 装饰器函数,参数 func 是被装饰的函数
def decorator(func):
    def wrapper(*args, **kw):
        print('在飞之前,我先给你加满油:'.format(func.__name__))
        # 真正执行的是这行。
        func(*args, **kw)
        print('飞完之后,我给你找个飞机场!')
    return wrapper

@decorator
def ceshi_decorator():
    print("我正在飞······")


print("被装饰之后,我们的函数的名称是:",ceshi_decorator.__name__)

输出结果为:

被装饰之后,我们的函数的名称是:wrapper

PS:上面wrapper的名称其实是上面装饰器函数里面的def wrapper(*args, **kw):的名称.

因为这种定义的方式,改变了内部的函数的名称:之前我们再定义我的flask脚手架的时候就遇到过一个问题,我想给我们的每个接口预分配一个权限标记的时候,就使用上面那种带参数的装饰器的方式,发现,每次启动装饰器的时候,只有部分的函数且是第一个的被装饰的函数气作用了!百思不得姐!

给我们的路由添加权限标记:

from backend.admin.api.sys.role import bp_edprint

from backend.common.validators import OrderBaseForm
from wtforms import StringField,IntegerField
from wtforms.validators import DataRequired
from backend.common.excs import Success, Fail
from backend.admin.services.logic_sys_role import LogicSysRole
from backend.admin.hooks.request_log import MakeRequesLogTag

class Form(OrderBaseForm):
    roleCode = StringField(validators=[DataRequired(message='角色编码 必须传入')])
    roleName = StringField(validators=[DataRequired(message='角色名称 必须传入')])
    description = StringField(default='')

@bp_edprint.route('/add',methods=['POST'],strict_slashes=False)
@MakeRequesLogTag(log_tag_name='添加角色')
def add():
    # ==============================================
    # 根据用户ID来修改的用户信息
    forms = Form().validate_for_api()
    # 新增角色
    _result = LogicSysRole.add_role(role_code=forms.roleCode.data.strip(), role_name=forms.roleName.data.strip(), description=forms.description.data.strip(), update_time='now()')

    return Success(api_code=0, result=None, message='添加成功!') if _result else Fail(api_code=0, result=None, message='添加失败!')

对应的装饰器函数是:

# 函数装饰器
def MakeRequesLogTag(log_tag_name):
    def wrapper(func):
        def deco(*args, **kwargs):
            request.reques_log_tag = log_tag_name
            return func(*args, **kwargs)
        return deco
    return wrapper

那么恭喜你,如果你使用上面这种形式,就只有最签名的一个初始化的时候回有用!其余的都不会起作用的!!!!

所以为了保持添加后内部函数名和函数的doc等不发生了改变,需要引入Python的functools包中提供了一个叫wraps的decorator来消除这样的副作用。

所以上面那个装饰器修改为下面代码示例就正常了!:

# 带参数且返回相关的值得装饰器
def MakeRequesLogTag(log_tag_name=None,operate_type = 1):
    """
    :param token_key: 默认的头部的信息(提交的头部信息)
    :param nees_check_perms: 需要的权限标记
    :param is_log_operation: 是否需要记录日志
    :return:
    "
""
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            # 头部提取的token信息进行校验
            request.reques_log_tag = log_tag_name
            request.operate_type = operate_type
            # print('token',token)

            print("标记",log_tag_name)

            return fn(*args, **kwargs)
        return wrapper
    return decorator

总结:Python的functools包提供的wraps的主要是用来被装饰的函数的名和逻辑修改,引发一些列异变。所以通常我们的写一个装饰器时候,最好是引入上functools的wrap,让它能能保留原有函数的名称和docstring,避免异变事件!

个人其他博客地址

简书:https://www.jianshu.com/u/d6960089b087

掘金:https://juejin.cn/user/2963939079225608

小钟同学 | 文 【原创】| QQ:308711822

  • 1:本文相关描述主要是个人的认知和见解,如有不当之处,还望各位大佬指正。
  • 2:关于文章内容,有部分内容参考自互联网,如有链接会声明标注;如没有及时标注备注的链接的,如有侵权请联系,我会立即删除处理哟。


原文作者:程序员小钟同学;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2021-02-20

  • 博文量
    224
  • 访问量
    111708