Python基础15-函数闭包与装饰器

目录

装饰器概念

装饰器的实现

修改被装饰函数的代码(非装饰器实现)

修改被装饰函数的调用方式(非装饰器实现)

装饰器的实现(不完整的实现level1)

装饰器实现的语法糖(不完整的实现level2)

函数闭包加上返回值和参数(基本完整的装饰器实现)

过渡内容

带参数的装饰器


装饰器概念

装饰器本质就是函数,功能是为其他函数添加附加功能。装饰器的原则如下

  1. 不修改被装饰函数的源代码
  2. 不修改被装饰函数的调用方式

装饰器的实现

要实现装饰器,就是遵循装饰器的原则来实现装饰器,具体做法就是 装饰器=高阶函数+函数嵌套/闭包。

我们用一个例子来实现以下功能。有个函数foo,执行时间较长,我们在不修改foo函数的源代码、不修改foo函数的调用方式的前提下,显示foo函数执行的时间。这个需求就用装饰器来实现,我们一点一点接近装饰器的完整实现。

import time


def foo():
    """
    模拟foo函数运行了3秒左右
    :return: 
    """
    time.sleep(3)
    print('from foo')
    pass


# foo函数的调用方式就是直接调用
foo()

修改被装饰函数的代码(非装饰器实现)

我们可以选择在foo函数开始执行的时候记下时间,在foo结束执行的时候再记下时间,从而得到foo函数的执行时间。但是这样就必须修改foo函数的源代码,这在大型项目里面很可能会引发连锁反应,导致其他调用的地方出错。

import time


def foo():
    """
    模拟foo函数运行了3秒左右
    :return:
    """
    start_time = time.time()
    time.sleep(3)
    print('from foo')
    stop_time = time.time()
    print("foo执行时间 %s" % (stop_time - start_time))
    pass


# foo函数的调用方式就是直接调用
foo()
# from foo
# foo执行时间 3.0006065368652344

修改被装饰函数的调用方式(非装饰器实现)

我们还可以保持foo函数不修改,将foo作为参数传给装饰器函数timer,由timer来记录foo的执行时间。这个样保证了foo代码不变,但是foo函数的调用方式发生了变化。

import time


def timer(func):
    start_time = time.time()
    func()
    stop_time = time.time()
    print("running time: %s" % (stop_time - start_time))
    return func


def foo():
    """
    模拟foo函数运行了3秒左右
    :return:
    """
    time.sleep(3)
    print('from foo')
    pass


timer(foo)
# from foo
# running time: 3.013805627822876

装饰器的实现(不完整的实现level1)

有没有既不修改被装饰函数的源代码、又不修改被装饰函数的调用方式,这样两全其美的实现呢?那就是装饰器。装饰器函数:将被装饰函数作为入参,这就需要定义高阶函数;将附加了装饰功能的函数作为返回值返回,同时保证被装饰函数在装饰器里面不被调用,这就需要函数嵌套/闭包。最后,要保证调用方式不变,那么就将返回值赋给被装饰函数。

具体到被装饰函数foo和装饰器timer的例子。就是foo要作为timer的入参,timer里面定义嵌套函数wrapper,wrapper被调用时foo和修饰功能都被执行,将wrapper作为返回值返回,返回值付给foo。

import time


def timer(func):
    def wrapper():
        start_time = time.time()
        func()
        stop_time = time.time()
        print("运行时间 %s" % (stop_time - start_time))
        pass

    return wrapper


def foo():
    """
    模拟foo函数运行了3秒左右
    :return:
    """
    time.sleep(3)
    print('from foo')
    pass


foo = timer(foo)
foo()
# from foo
# 运行时间 3.012805223464966

装饰器实现的语法糖(不完整的实现level2)

上面的实现还有一个问题,那么就是每次都要经过装饰器返回值付给被修饰函数的过程。这个Python在语法层面予以解决。那就是把  @装饰器  放在被装饰函数定义的前一行,这样就装饰了。

import time


def timer(func):
    def wrapper():
        start_time = time.time()
        func()
        stop_time = time.time()
        print("运行时间 %s" % (stop_time - start_time))
        pass

    return wrapper


@timer
def foo():
    """
    模拟foo函数运行了3秒左右
    :return:
    """
    time.sleep(3)
    print('from foo')
    pass


foo()
# from foo
# 运行时间 3.012805461883545

函数闭包加上返回值和参数(基本完整的装饰器实现)

上面实现的装饰器还不够完整。如果被装饰的函数还有入参和返回值,那么上面的实现方式是不行的。我们来解决这个问题。

首先,如果被装饰函数有入参,在执行装饰器返回的嵌套函数里面也要有相应的入参。为了保证装饰器适应性任意多个入参,这个嵌套函数应该有变长的参数,因此wrapper的入参是*args,**kwargs。调用func的时候也应当相应的加参数*args,**kwargs。注意,这两个*args,**kwargs的含义是不一样的。wrapper的参数是函数定义,回忆函数基础那篇博客变长参数的部分,这里的args和kwargs分别是元组和字典。func调用的地方是函数调用的入参,是解压序列,将args元组解压序列成位置参数,将kwargs解压序列成关键字参数。因此,在装饰器实现的时候,都应保证wrapper和func的参数是*args,**kwargs。

其次,如果被装饰函数有返回值,在执行装饰器返回的嵌套函数里面也要有相应的返回值。解决办法就是将返回值记录下来。在适当的地方返回。

import time


def timer(func):
    def wrapper(*args, **kwargs):  # 这里的*args, **kwargs是函数定义,表示可边长参数
        start_time = time.time()
        res = func(*args, **kwargs)  # 这里的*args, **kwargs是函数调用的入参,将wrapper的参数解压序列
        stop_time = time.time()
        print("运行时间 %s" % (stop_time - start_time))
        return res
        pass

    return wrapper


@timer
def foo(os, db, app):
    """
    模拟foo函数运行了3秒左右
    :return:
    """
    time.sleep(0.2)
    res = 'os->%s,db->%s,app->%s' % (os, db, app)
    print('from foo')
    return res
    pass


RES = foo('linux', db='mysql', app='apache')
print(RES)
# from foo
# 运行时间 0.20280051231384277
# os->linux,db->mysql,app->apache

过渡内容

这一部分为后面做铺垫,例子完全来自我所看视频教程的内容。需求是,在首页index、个人中心home和购物车shopping_car执行前进行身份验证,不能修改这三个函数的调用方式。

def index():
    print('welcome to front page')
    pass


def home(name):
    print('welcome home%s' % name)
    pass


def shopping_car(name):
    print('%s\'s shopping car has [%s,%s,%s]' % (name, 'food', 'drink', 'toy'))
    pass


index()
home('kevin')
shopping_car('kevin')
# welcome to front page
# welcome homekevin
# kevin's shopping car has [food,drink,toy]

用全局变量来模拟数据库记录和当前用户session。代码如下。

# 用户列表,模拟数据库存储用户信息
user_dic = [
    {'username': 'alice', 'password': '123'},
    {'username': 'kevin', 'password': '123'},
    {'username': 'bob', 'password': '123'},
    {'username': 'charlie', 'password': '123'},
]

# 当前用户,模拟session
curr_user = {'username': None, 'login': False}


def auth_func(func):
    def auth_check(*args, **kwargs):
        # 如果有session,直接执行功能
        if curr_user['username'] and curr_user['login']:
            res = func(*args, **kwargs)
            return res
        # 否则验证身份
        username = input('username:').strip()
        password = input('password:').strip()
        for user in user_dic:
            if username == user['username'] and password == user['password']:
                # 身份验证成功,更新session,并执行功能
                curr_user['username'] = username
                curr_user['login'] = True
                res = func(*args, **kwargs)
                return res
            pass
        # 验证不成功,报错
        print('wrong username or password!')
        pass

    return auth_check


@auth_func
def index():
    print('welcome to front page')
    pass


@auth_func
def home(name):
    print('welcome home, %s' % name)
    pass


@auth_func
def shopping_car(name):
    print('%s\'s shopping car has [%s,%s,%s]' % (name, 'food', 'drink', 'toy'))
    pass


index()
home('kevin')
shopping_car('kevin')
# username:kevin
# password:123
# welcome to front page
# welcome home, kevin
# kevin's shopping car has [food,drink,toy]

带参数的装饰器

接上面过渡内容,如果我们的验证方式有不同,index用数据库,home和shopping_car用其他的验证方式,那怎么办呢?可以用带有参数的装饰器。我们在上面auth_func外再包一层auth,再调用装饰器的时候,调用auth执行的结果。代码如下。

# 用户列表,模拟数据库存储用户信息
user_dic = [
    {'username': 'alice', 'password': '123'},
    {'username': 'kevin', 'password': '123'},
    {'username': 'bob', 'password': '123'},
    {'username': 'charlie', 'password': '123'},
]

# 当前用户,模拟session
curr_user = {'username': None, 'login': False}


def auth(auth_type='filedb'):
    def auth_func(func):
        def auth_check(*args, **kwargs):
            # 这里通过auth_type可以做更多的选择
            if auth_type == 'filedb':
                # 如果有session,直接执行功能
                if curr_user['username'] and curr_user['login']:
                    res = func(*args, **kwargs)
                    return res
                # 否则验证身份
                username = input('username:').strip()
                password = input('password:').strip()
                for user in user_dic:
                    if username == user['username'] and password == user['password']:
                        # 身份验证成功,更新session,并执行功能
                        curr_user['username'] = username
                        curr_user['login'] = True
                        res = func(*args, **kwargs)
                        return res
                    pass
                # 验证不成功,报错
                print('wrong username or password!')
            elif auth_type == 'ldap':
                print('ldap check')
                res = func(*args, **kwargs)
                return res
            else:
                print('what the fuck!')
            pass

        return auth_check

    # 将auth_func返回
    return auth_func


# @ auth调用的结果
# 也就相当于@auth_func
@auth()
def index():
    print('welcome to front page')
    pass


# @ auth调用的结果
# 也就相当于@auth_func
@auth(auth_type='ldap')
def home(name):
    print('welcome home, %s' % name)
    pass


# @ auth调用的结果
# 也就相当于@auth_func
@auth(auth_type='kerbers')
def shopping_car(name):
    print('%s\'s shopping car has [%s,%s,%s]' % (name, 'food', 'drink', 'toy'))
    pass


index()
home('kevin')
shopping_car('kevin')
# username:kevin
# password:123
# welcome to front page
# ldap check
# welcome home, kevin
# what the fuck!

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

苦行僧(csdn)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值