面试中经常被问到的——什么是装饰器?

Python中的装饰器

前言

变量作用域规则

在讲装饰器之前,我们先来介绍一下 Python 中变量的作用域,Python中的作用域一共有四种,依次是L->E->G->B

  • L(Local): 局部作用域
  • E(Enclosing): 闭包函数外的函数中
  • G(Global):全局作用域
  • B(Built-in): 内建作用域

注:搜索一个标识符时会按照LEGB的顺序进行搜索,如果所有的作用域中都没有找到这个标识符就会引发NameError异常

一等对象

”编程语言理论家“定义了一个特殊的概念一等对象,满足如下条件的对象都可被称为一等对象

  • 在运行时创建
  • 能赋值给变量或者数据结构中的元素
  • 能作为参数传给函数
  • 能作为函数的返回结果

Python中,函数和整数字符串字典一样都是一等对象,如果你觉得上面的定义有点抽象的话,记住下面两条就行

1.函数名存放的是函数所在空间的地址,函数名()执行函数名所存放空间地址中的代码
2.函数名也可以像普通的变量一样赋值

闭包

闭包是指延伸了作用域的函数,其中包含了在函数定义体中引用但不在函数定义体中定义的非全局变量
大家不用纠结闭包到底是个什么东西,把上面的那句话去掉所有修饰语,保留主语其实闭包就是闭包=内嵌函数+(特殊)非全局变量

创建一个闭包必须满足:

  • 必须有一个内嵌函数的前提下(内嵌函数:函数里面定义函数)
  • 内嵌函数必须引用外部函数中的变量【包括外部函数中的参数】
  • 外部函数的返回值必须是内嵌函数【函数作为一等对象的体现:把函数像普通变量一样返回】

闭包示例

def make_averager():
    series = []		# series 是属于 make_averager 函数中的局部变量

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total / len(series)

    return averager	


avg = make_averager()    # 调用 make_averager 时返回 averager 函数对象
print(avg(10))	# 结果为 10.0 此时series = [10]
print(avg(11))	# 结果为 10.5 此时series = [10, 11]
print(avg(12))	# 结果为 11.0 此时series = [10, 11, 12]

理应来说,在执行完 avg = make_averager()这一步,make_averager函数已经返回,它的本地作用域就被回收了,为什么它的内部函数 averager() 还能使用外部函数make_averager中定义的局部变量呢?

对此书上给的解释如下:
在这里插入图片描述
同学们不要纠结于闭包二字这个陌生又抽象的概念,还有官方给出的莫名其妙的概念,博主把闭包的定义读了十遍才勉强弄明白这是个什么玩意儿,从上面的图片中一目了然,黑色框框圈出来的一个整体就是一个闭包

闭包即(内嵌)函数 + (外部函数中定义的)非全局变量

书上还给这种(外部函数中定义的)非全局变量取了一个名字叫自由变量,这些专家就喜欢定义新概念把人搞的云里雾里,不过叫起来确实方便一些【doge】

还需要注意一点的是
错误示范:

def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        count += 1      # 如果我们在内部函数的定义中重新为count赋值了,这会把count变为内部函数的局部变量
        total += new_value	# total也是如此
        return total / count

    return averager

对于数字字符串元组等不可变类型来说,只能读取,不能更新,如果在内部函数中重新进行赋值操作,python的解释器起始会隐式的创建属于内部函数的局部变量,外部函数中的变量就被覆盖了,因此这就不是一个闭包了

正确示范

def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        nonlocal count, total	# 加上nonlocal声明
        count += 1  
        total += new_value
        return total / count

    return averager

Python3中引入了 nonlocal声明,即使在内部函数中为变量重新赋值,其实用的还是外部函数中定义的变量,只是更新了值

注:之所以在讲装饰器之前花这么大的篇幅给大家介绍闭包的原理是因为如果在面试中或者在以后工作的实际开发中,如果对闭包的概念不清楚的话就无法自己手动实现一个装饰器

一、装饰器初识

我们先来看一个装饰器的经典案例,这也是面试中问到装饰器的一道手写题:写一个记录函数执行时间的装饰器

from time import time


def record_time(func):
    def inner(*args, **kwargs):  # 这里的*args和**kwargs用来接收被装饰的函数的参数
        """函数注释:用来修饰被装饰函数的内嵌函数"""
        start = time()
        result = func(*args, **kwargs)  # result用来接受被装饰的函数的返回值
        end = time()
        print(f"被装饰的函数{func.__name__}的执行时间为:{end - start}")
        return result + '~~~~~~'   # 对被装饰的函数的返回值进行修饰

    return inner

上面定义的record_time函数即为一个装饰器,内嵌函数inner + 外部函数的参数func即构成了闭包,

  • 使用方法1
def countdown(n):
    """函数注解:这是一个倒计时函数"""
    print("开始倒计时")
    while n > 0:
        n -= 1
    print("倒计时结束")
    return "我执行完啦"


# 使用方法1
countdown = record_time(countdown) # countdown是 record_time 函数中返回的 inner 函数
res = countdown(1000000)   # 执行 inner 函数,将 inner 函数的返回值给 res
print(res)
  • 使用方法2
@record_time	# 对人类更友好的语法糖写法
def countdown(n):
    """函数注解:这是一个倒计时函数"""
    print("开始倒计时")
    while n > 0:
        n -= 1
    print("倒计时结束")
    return "我执行完啦"

res = countdown(10000000)   # 被装饰完的 countdown 虽然看起来和调用方式都是 countdown,其实是它的本质是一个 inner 函数
print(res)

写法1和写法2的原理是一样的,写法2是语法糖的写法,同学们可能会疑惑什么是语法糖,语法糖指那些没有给计算机语言添加新功能,而只是对人类来说更“甜蜜”的语法【其实就是装X的一种说法】,大家不要被陌生词汇吓到

  • 语法糖的书写规范:紧贴在被装饰对象的上方书写,语法糖语被装饰对象之间不可以有代码
  • 语法糖的内部原理:语法糖会自动把被装饰对象的名字当成参数传给装饰器函数调用

把需要装饰的函数作为参数传进来,在内嵌函数中进行加工,被装饰的函数【这里是countdown】可能会有一些参数,故内嵌函数使用*args**kwargs来接受任意类型的参数

装饰器扩展【了解即可】

你写了一个装饰器作用在某个函数上,但是这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了

from time import time
from functools import wraps


def record_time(func):
    @wraps(func)
    def inner(*args, **kwargs):  # 这里的*args和**kwargs用来接收被装饰的函数的参数
        """函数注解:用来修饰被装饰函数的内嵌函数"""
        start = time()
        result = func(*args, **kwargs)  # result用来接受被装饰的函数的返回值
        end = time()
        print(f"被装饰的函数{func.__name__}的执行时间为:{end - start}")
        return result + '~~~~~~'  # 对被装饰的函数的返回值进行修饰

    return inner


@record_time
def countdown(n):
    """函数注解:这是一个倒计时函数"""
    print("开始倒计时")
    while n > 0:
        n -= 1
    print("倒计时结束")
    return "我执行完啦"


res = countdown(10000000)   # 被装饰完的countdown虽然看起来和调用方式都是countdown,其实是它的本质是一个inner函数
print(res)
print(countdown.__name__)   # 函数名
print(countdown.__doc__)    # 函数注释
print(countdown.__annotations__)    # 函数注解

在实际的软件开发中,各个函数的功能都是封装好的,这个时候当我们定位问题的时候,如果我们发现整个程序运行的耗时的话,但是我们又不知道具体是在哪个函数中耗时较【超出预期时间】,我们需要统计每个函数的执行时间,进行控制台输出或者打印到日志里面进行观测,这就需要我们动别人写好的代码,如果修改不当可能导致整个程序无法运行,或者你要在每个函数中在开始之前打印时间,在函数执行结束后统计时间差,这样就变成了一个重复繁琐的工作,而上面的这个装饰器就可以帮你在需要统计的时间的函数前面装饰一下,不用统计的就不管,之前的函数仍然可以正常运行,并附加了额外的功能,这里是记录时间,当然也可以自定义或者私人订制其他功能

二、装饰器的原理与作用

从上面的案例中我们现在可以知道装饰器的本质就是一个函数,是闭包的一种实现,它的作用是让其它函数在不需要做任何代码变动的前提下增加额外的功能

使用装饰器的时候,它接受一个函数【被装饰的函数】作为参数并返回一个新的函数对象

你可以在装饰器的内部函数中实现需要额外增加的功能,虽然被装饰的函数的调用方法没有改变,但实际上已经不是原来的函数,而是变成了装饰器返回的函数对象

装饰器的介绍

  • 1.带参数的装饰器的简单示例

1.带参装饰器本质上就是在普通装饰器头上再嵌套一层函数用来传递参数
2.当我们使用最外层的装饰器函数传递参数时,语法糖的写法再后面的括号中也需要加上相对应的参数

def repeat(count):
    """
    函数功能描述:最外层的装饰器函数 repeat() 只是用来向内层函数传递参数 count 的一个装饰器
    :param count: count:表示被修饰函数需要重复执行的次数
    :return: 
    """
    def decorator(func):
        """
        函数功能描述:在 repeat() 函数内部定义的 decorator() 才是我们用来接受被修饰函数 func 的装饰器函数 
        :param func: 被修饰的函数
        :return: 
        """
        def inner(*args, **kwargs):
            for i in range(count):
                func(*args, **kwargs)

        return inner

    return decorator


@repeat(3)	# 使用方法
def greet(name):
    print("Hello, %s!" % name)


greet("Tom")  # 当我们执行 greet('Tom') 函数时,它实际上已经被修饰成了 repeat(3)(greet)('Tom') 这样的形式

【进阶】:面试中的实战案例

有一个通过网络获取数据的函数(可能会因为网络原因出现异常),写一个装饰器让这个函数在出现指定异常时可以重试指定的次数,并在每次重试之前随机延迟一段时间,最长延迟时间可以通过参数进行控制

from random import random, randint
from time import sleep


def retry(retry_times, max_wait_sec=5):
    def decorator(func):
        def inner(*args, **kwargs):
            for _ in range(retry_times):
                try:
                    return func(*args, **kwargs)

                except Exception as e:  # 出现指定异常
                    print(f"出现异常:{e}, 延迟{random() * max_wait_sec}重试....")
                    sleep(random() * max_wait_sec)
            return None

        return inner

    return decorator


@retry(retry_times=3)
def get_data():
    if randint(1, 8) > 4:
        raise Exception
    else:
        print("获取到数据")


get_data()
  • 2.装饰器的叠加使用

我们先来看一下下面这个示例:

def outter1(func1):
    print('加载了outter1')

    def wrapper1(*args, **kwargs):
        print('执行了wrapper1')
        res1 = func1(*args, **kwargs)
        return res1 + 'wrapper1包装返回~ '

    return wrapper1


def outter2(func2):
    print('加载了outter2')

    def wrapper2(*args, **kwargs):
        print('执行了wrapper2')
        res2 = func2(*args, **kwargs)
        return res2 + 'wrapper2包装返回~ + '

    return wrapper2


def outter3(func3):
    print('加载了outter3')

    def wrapper3(*args, **kwargs):
        print('执行了wrapper3')
        res3 = func3(*args, **kwargs)
        return res3 + "wrapper3包装返回~ + "

    return wrapper3


@outter1  
@outter2  
@outter3  
def index():
    print('from index')
    return 'index 函数的返回值 + '


res = index()	# res函数用于接受index函数的返回值
print(res)

# 控制台输出结果如下:
# 加载了outter3
# 加载了outter2
# 加载了outter1
# 执行了wrapper1
# 执行了wrapper2
# 执行了wrapper3
# from index
# index 函数的返回值 + wrapper3包装返回~ + wrapper2包装返回~ + wrapper1包装返回~ 

三层包装的效果等于 index = outter1(outter2(outter3(index)))

经过三层包装之后的index函数其实是一个wrapper1

  • 实际调用index()会执行wrapper1会执行到里面的func1(即wrapper2)
  • 接着去执行wrapper2wrapper2执行到一半会走到里面的func2(即wrapper3)
  • 接着去执行wrapper3wrapper3执行到一半会走到里面的func3(即index)
  • 执行完index后将index的返回值依次返回,wrapper3接着往下执行
  • wrapper3执行完后对index的返回值进行修饰加工后返回,wrapper2接着往下执行
  • wrapper2执行完后对wrapper3的返回值进行修饰加工后返回,wrapper1接着往下执行
  • wrapper1即加工后的index,实际调用index()的返回值即wrapper1的返回值

【进阶】: 叠放装饰器在实际开发中的应用:实现认证和授权功能

def authenticate(func):
    def wrapper(*args, **kwargs):
        print("开始认证...")

        if args[0].is_authenticated:    # 假设在args[0]中保存了当前用户的信息

            result = func(*args, **kwargs)
            return result
        else:
            print("用户未认证: User is not authenticated")

    return wrapper


def authorize(roles):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print("开始用户角色权限校验...")
            if args[0].role in roles:     # 假设在args[0]中的用户角色的权限在所给的角色权限列表中
                result = func(*args, **kwargs)
                return result
            else:
                print("用户未授权")

        return wrapper

    return decorator


class User:
    def __init__(self, name, role):
        self.name = name
        self.role = role
        self.is_authenticated = True  # 认证标识:默认为True,表示实例化出来的User默认为已认证的状态


@authenticate
@authorize(["管理员账号", "会员账号"])
def secret_page(user):
    return "认证成功, 开始访问秘密页面 ! ! !"


user = User("Alice", "管理员账号")
res = secret_page(user)
print(res)

user = User("Tom", "访客账号")
res = secret_page(user)
print(res)

三、装饰器的使用场景

插入日志、性能测试、事物处理、缓存、权限校验

1.权限校验应用示例:

def login_auth(func):
    def login(*args, **kwargs):
        if is_login['is_login']:
            print("已经登录过了,无需再次登录")
            func(*args, **kwargs)
        else:
            username = input('用户名:').strip()
            password = input('密码:').strip()
            if username == 'kevin' and password == '123':
                print("登录成功")

                func(*args, **kwargs)
                is_login['is_login'] = True
            else:
                print("用户名或密码错误!")

    return login


def taobao_index():
    print("开始访问淘宝首页!")


@login_auth
def personal_homepage():
    print("开始访问个人主页~")


@login_auth
def order_page():
    print("开始访问订单页面!")


if __name__ == '__main__':
    is_login = {'is_login': False}  # 是否登录标志
    taobao_index()      # 访问淘宝首页
    personal_homepage()  # 需要进行登录才能访问个人主页
    order_page()         # 如果之前已经登陆过则直接访问订单页面

2.实现缓存机制,避免重复计算

下面的这个函数是一个用来计算第 n 个斐波那契数的函数,斐波那契数列大家应该都有听说过~

def fibonacci(n):
    """这是一个求第 n 个斐波那契数的递归函数"""
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)

如果计算fibonacci(10),程序的执行顺序如下面这张图一样,从图中我们可以看到由于递归函数的存在,有大量重复的计算,所以程序的执行会非常的耗时,fibonacci(8)执行了两次,fibonacci(7)执行了三次,fibonacci(6)执行了四次,这是因为对于我们已经计算出来的中间结果没有进行保存,所以重复调用重复计算,再次强调,这是一个非常耗时的操作 ! ! !
在这里插入图片描述

如下代码中所示,我们手动实现了一个memory装饰器把fibonacci函数装饰城inner函数,在inner函数内部实现对中间结果的缓存,如果要计算的数在缓存中则直接返回对应的fibonacci数,否则才调用fibonacci函数去进行递归计算

def memory(func):
    cache_dict = {}     # 用一个字典作为容器保存中间结果,key 为 n,value 为第 n 个数对应的fibonacci数

    def inner(key):
        if key in cache_dict:   # 如果要计算的数在 cache_dict 容器中
            return cache_dict[key]  # 直接返回对应的 value,这样就不用再次进入底层递归计算
        else:
            res = func(key)     # 否则就老老实实通过递归计算该数对应的 fibonacci 数
            cache_dict[key] = res   # 将计算的结果保存起来
            return res

    return inner


@memory
def fibonacci(n):
    """
    这是一个求第 n 个斐波那契数的递归函数
    """
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


value = fibonacci(40)
print(value)

3.错误处理

假设一个这样的场景,一个公司新招了几个小白程序员,他们各自写的代码分别如下
小白A:实现两个数相除,将结果返回给函数调用者

def division(numerator, denominator):
    """
    :param numerator: 分子(被除数)
    :param denominator: 分母(除数)
    :return:
    """
    res = numerator / denominator
    return res

小白B:实现两个数相加,将结果返回给函数调用者

def two_num_sum(a, b):
    c = a + b
    return c    # 返回两个数的和

小白C:找到列表中的第四个元素并返回给函数调用者

def find_list(lis):
    return lis[3]   # 返回列表中的第四个元素

写完之后,公司的技术负责人一看,把他们全都开了,连个异常处理都没有怎么程序不是分分钟就崩了,要知道用户都是憨憨,你不能和他们说你用我的函数要传什么类型的参数,要注意什么,用户全凭自己开心,想怎么来怎么来,如果用户瞎传,你的程序就直接抛出异常崩溃了,影响了整个程序的运行岂不是用户体验1.0,开了三个小白后经理找来你这个高级开发来解决这个问题,如果你不懂装饰器,你可能需要对每个小白写的代码一个一个的改,加上异常处理,但你看了这篇文章之后你学会了装饰器,所以对你来说洒洒水啦~

你写了下面这样的一个catch_exception装饰器函数,然后加在了每个小白写的代码上,问题就轻松解决啦!你说好麻烦,要改好几天,结果你几分钟搞定,然后摸鱼…

def catch_exception(func):
    def inner(*args, **kwargs):
        try:
            result = func(*args, **kwargs)
            return result
        except ZeroDivisionError:	# 捕获除 0 异常
            print("傻×,分母不能为0")
        except TypeError:		# 捕获类型错误
            print("傻×,你是不是搞错数据类型啦~")
        except IndexError:	# 捕获索引错误
            print("傻Ⅹ,索引超出范围啦~")

    return inner


@catch_exception
def division(numerator, denominator):
    """
    :param numerator: 分子(被除数)
    :param denominator: 分母(除数)
    :return:
    """
    res = numerator / denominator
    return res


@catch_exception
def two_num_sum(a, b):
    c = a + b
    return c    # 返回两个数的和


@catch_exception
def find_list(lis):
    return lis[3]   # 返回列表中的第四个元素


if __name__ == '__main__':
    division(3, 0)      # 分母传了一个 0
    two_num_sum(4, 'aa')    # 给了两个不同类型的数
    find_list([1, 2, 3])    # 给了一个只有三个元素的列表

4.记录日志

为了照顾到第一次接触logging库的同学,博主在这里花一点篇幅介绍一下,logging库是属于python的一个库,它主要是用来帮助我们实现记录日志【一般用于开发调试或测试中】的目的,默认分为六种日志级别(括号为级别对应的数值),NOTSET:0DEBUG:10INFO:20WARNING:30ERROR:40CRITICAL:50。我们自定义日志级别时注意不要和默认的日志级别数值相同,logging 执行时输出大于等于设置的日志级别的日志信息,如设置日志级别是 INFO,则 INFOWARNINGERRORCRITICAL 级别的日志都会输出

其中logging大概分为四个组件

组件名称对应类名功能描述
日志器logger提供了应用程序可一直使用的接口
处理器Handler将日志记录器产生的日志记录发送至合适的目的地
过滤器Filter提供了更细粒度的控制工具来决定输出哪条日志,丢弃哪条日志
格式器Formatter决定日志记录的最终输出格式
logging.basicConfig介绍

basicConfig函数是logging模块中的一个配置函数用于对logging进行基本配置
basicConfig函数可以接受多个参数用于设置日志的格式、日志级别、输出位置等信息。常用的参数如下:

参数名称参数描述
filename日志输出到文件的文件名,如果不指定则默认输出到控制台
filemode指定日志输出文件的打开模式,可选值有rwa
format日志输出的格式, 可选值有asctimenamelevelnamemessage等参数
datefat日志附带日期时间的格式
level设置日志输出的级别,可选值有DEBUGINFOWARNINGERRORCRITICAL,默认为WARNING
stream定义输出流用来初始化StreamHandler对象,不能和filename参数一起使用,否则会引发ValueError异常
handles定义处理器用来创建Handler对象,不能和filename,stream参数一起使用,否则也会抛ValueError异常
import logging

logging.basicConfig(filename="test.log", filemode="w", format="%(asctime)s %(name)s:%(levelname)s:%(message)s",
                    datefmt='%Y-%m-%d %H:%M:%S', level=logging.DEBUG)
                    
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')

生成日志文件test.log,内容如下

2023-11-19 16:05:35 root:DEBUG:This is a debug message
2023-11-19 16:05:35 root:INFO:This is an info message
2023-11-19 16:05:35 root:WARNING:This is a warning message
2023-11-19 16:05:35 root:ERROR:This is an error message
2023-11-19 16:05:35 root:CRITICAL:This is a critical message
4.2 自定义 LOGGER

上面的示例中的基本使用可以让我们快速上手logging模块,但一般不能满足实际使用,我们还需自定义Logger,系统中的Logger对象不能被直接实例化,获取Logger对象的方法为getLogger(),并且这里运用了单例模式

这里的单例模式并不是说只有一个Logger对象,而是指整个系统只有一个root Logger对象,其他 Logger 对象在执行 info()error()等方法实际上调用都是root Logger对应的info()error()等方法

我们可以创造多个Logger对象,但是真正输出日志的是root Logger对象,每个Logger对象可以设置一个名字。另外每个对象可以设置多个Handler对象和Filter对象,Handler对象又可以设置多个Formatter对象,Formatter对象用来设置具体的输出格式,常用变量格式如下所示

变量格式变量描述
asctime%(asctime)s构造日志的事件格式,默认情况下是精确到毫秒,可以额外指定datefmt 参数自定义格式

注意:如果需要对不同的模块分别进行日志记录,则需要创建不同的Logger对象,并对其进行配置,可以使用Logger类的方法来设置日志级别、日志格式、输出位置等信息,例如addhandler方法可以添加一个处理器来指定日志输出到文件还是输出到网络等位置

注意:如果需要对不同的模块分别进行日志记录,则需要创建不同的Logger对象,并对其进行配置,可以使用Logger类的方法来设置日志级别、日志格式、输出位置等信息,例如addhandler方法可以添加一个处理器来指定日志输出到文件还是输出到网络等位置

好了,关于logging模块这里不做过多介绍,网上有很多优秀的文章比博主这里的介绍要详细并且通俗易懂得多,我们现在再来看一下装饰器在记录日志中的应用吧,假设我们有一个函数calculate,它接受一个整数作为参数,并计算该整数的阶乘,现在我们希望在调用该函数的时候自动记录日志输出以便于调试和监控程序的运行情况

于是我们定义了一个装饰器函数log,在装饰器函数中我们使用Python标准库中的logging模块来设置日志输出格式和目标文件,然后我们返回一个新的函数wrapper,在wrapper函数中我们使用 logging.debug()记录函数的调用信息,然后调用被装饰的函数,将返回值保存在result变量中,最后再使用 logging.debug()函数记录返回的信息

import time
import logging

logging.basicConfig(level=logging.DEBUG,
                    format="%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s",
                    datefmt='%Y-%m-%d %H:%M:%S',  # 精确到秒
                    filename="calculate函数的运行日志.log",
                    filemode="w")


def log(func):
    def wrapper(*args):
        logging.debug('Calling Function:{} with args:{}'.format(func.__name__, args))
        result = func(*args)
        logging.debug('Function: {} Return: {}'.format(func.__name__, result))
        return result

    return wrapper


@log
def calculate(n):
	"""
	计算阶乘的递归函数
	"""
    if n == 0:
        return 1
    else:
        time.sleep(0.5)
        return n * calculate(n - 1)


# 调用函数,同时记录输出
print(calculate(5))

calculate函数的运行日志.log

2023-11-21 21:32:17 example.py[line:14] DEBUG Calling Function:calculate with args:(5,)
2023-11-21 21:32:17 example.py[line:14] DEBUG Calling Function:calculate with args:(4,)
2023-11-21 21:32:18 example.py[line:14] DEBUG Calling Function:calculate with args:(3,)
2023-11-21 21:32:18 example.py[line:14] DEBUG Calling Function:calculate with args:(2,)
2023-11-21 21:32:19 example.py[line:14] DEBUG Calling Function:calculate with args:(1,)
2023-11-21 21:32:19 example.py[line:14] DEBUG Calling Function:calculate with args:(0,)
2023-11-21 21:32:19 example.py[line:16] DEBUG Function: calculate Return: 1
2023-11-21 21:32:19 example.py[line:16] DEBUG Function: calculate Return: 1
2023-11-21 21:32:19 example.py[line:16] DEBUG Function: calculate Return: 2
2023-11-21 21:32:19 example.py[line:16] DEBUG Function: calculate Return: 6
2023-11-21 21:32:19 example.py[line:16] DEBUG Function: calculate Return: 24
2023-11-21 21:32:19 example.py[line:16] DEBUG Function: calculate Return: 120

5.使用装饰器实现单例模式

本文关于正文的介绍到此结束,最后博主在这给大家分享一下本人在求职中碰到的一场面试,面试中面试官要求在指定的时间内实现一个装饰器

面试实战案例分享【插曲】

以下这道关于装饰器的题是博主本人面试中碰到的一道原题,给了我30分钟的让我手动实现一个装饰器实现如下的功能,大家可以一起来看一看做一做,看看你是否能完美解答
在这里插入图片描述
我给出的答案如下

import time
from functools import wraps


def decorator(func):
    count = 1
    redis = {}

    @wraps(func)
    def inner(*args, **kwargs):
        nonlocal count
        start = time.time()

        if args[0] in redis.keys():
            print(f"这是{func.__name__}{count}次调用,耗时时间为:{time.time() - start}")
            count += 1
            return "返回缓存中的数据" + redis[args]
        else:
            print(f"这是{func.__name__}{count}次调用")
            res = func(*args, **kwargs)
            redis[args] = res
            print(f"首次调用的耗时时间为:{time.time() - start}")
            count += 1
            return "首次调用返回的结果为:" + res

    return inner


@decorator
def my_func(word):
    time.sleep(2)
    return word.upper()


if __name__ == '__main__':
    print(my_func("hero"))
    print(my_func("hero"))
    print(my_func("hero"))
    print(my_func("her"))

博主给出的并不是最优解,只是在最短的时间内能够实现功能的一种解法,如果屏幕前的你有更好的思路,欢迎在评论区留言探讨~

结语

本文重点给大家介绍了装饰器的原理和应用,装饰器在实际软件开发和测试中的应用非常常见,是作为一个高级开发的必备技能之一,你不仅要能在协同合作开发中看懂别人写的装饰器,还要能写出自己的装饰器,要真正的理解装饰器的原理,对闭包的理解要深刻,装饰器的实际应用场景太多太多了,博主根本无法在这里介绍完,但是当你真的看完本节的知识点,将所有案例都能在自己的本地IDE实现或者调试一番,相信你一定能写出非常实用的装饰器案例~,最后的最后,如果你觉得本文对你有帮助的话,还请大家不吝点赞哦,一起加油 !

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

code_lover_forever

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

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

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

打赏作者

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

抵扣说明:

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

余额充值