322-装饰器

闭包的作用就是为了编写装饰器
装饰器的本质就是一个闭包

装饰器定义

就是给已有函数增加额外功能的函数,它本质上就是一个闭包函数

装饰器的功能特点:
①不修改已有函数的源代码
②不修改已有函数的调用方式
③给已有函数增加额外的功能

闭包这么编程装饰器的?

装饰器的雏形(下面都是没有返回值的函数)

栗子1:

在发评论函数前加一个登录的功能

提出需求:在不改变源函数以及源函数调用方式的前提下,为其添加一个权限验证(请先登录)
不改变源函数、不改变原有调用方式、需要增加权限验证(额外功能)

定义一个装饰器:本质:闭包函数(①有嵌套②有引用③有返回),而且装饰器函数本身必须有一个fn参数,代表要装饰的函数名称,当我们调用装饰器的时候,系统会自动将要装饰函数的名称发给fn参数

def logging(fn):  # 一般都叫fn
	def inner():
		# 引用fn函数之前,增加额外功能
		print('这里是登录操作')
		# 引用局部变量fn
		fn()
	return inner

@logging  # 语法糖🍭 
def comment():
	print('这里是发表评论操作')

comment()  # 执行发表评论函数

当调用语法糖的时候会自动把comment()作为参数传给fn

调试的代码执行顺序

  1. @语法糖首先被执行
  2. 执行装饰器函数把要装饰的函数名称传递给fn参数
  3. 执行装饰器内部的inner函数,表现为执行函数名称comment
  4. 当装饰器执行fn()的时候,原函数真正被执行

comment()是最后一步执行的

装饰器的原始实现:(现在都是用上面的语法糖来实现)

Tip

装饰器调用的本质↓:
func= logging(comment))只不过把func写成comment就和装饰器完全一致了
再调用comment()的时候本质就是调用inner()了
因为装饰器不能改变原来函数的代码和调用方式
所以用语法糖的格式来写

# 定义一个装饰器(①不能改变源代码②不能改变原有函数的调用方式)
def logging(fn):
	# fn = comment
	def inner():
		print('请先登录')
		fn()
	return inner
def comment():
	print('发表评论')
	
comment = logging(comment)  # 给comment添加装饰器,变量名称用同名,这样就不会改变原有调用方式
# comment = Logging() 返回结果 => inner
comment()  # 这里的comment已经不是原先的comment了
# comment() => inner()

栗子2:实际应用:求程序执行时间装饰器(装饰器带不定长参数(自己加 的))

在不改变原函数代码和调用方式的前提下,为其添加统计执行时间的功能:可以使用装饰器 -> 有嵌套、有引用、有返回
看这个例子足够了,这里涵盖很多知识点,包括下面的带参数的装饰器知识

import time
def get_time(fn):
	def inner(*args, **kwargs):  # 装饰器带任意参数
		start = time.time()
		fn(*args, **kwargs)
		end = time.time()
		print(f'{fn.__name__}函数执行时间:{end - start:.2f}')  # 获取函数名;格式化输出
	return inner

@get_time
def ikun(b):
	a = []
	for i in range(b):
		a.append(i * 'ikun')

ikun(10000)

装饰器装饰带有参数的函数for通用装饰器

栗子3:装饰器装饰带有参数的函数,函数几个参数,inner接收几个参数

需求:①在不改变原有函数代码②不改变原有函数调用方式的前提下③为其新增一个输出日志功能
注:实际日志应该写入到日志文件,但是还没学,这里就用print代替
需求:在输出结果之前,添加一个打印日志的功能=>
print('--日志信息:正在努力进行计算--')

def logging(fn):
	def inner(a, b):
	#添加额外要增加的功能
	print('-----日志信息:正在努力进行计算-----')
	fn(a, b)  #实际执行sum_num函数
	return inner
	
#定义一个函数sum_num(),针对参数求和
@logging
def sum_num(num1, num2):
	result = num1 + num2
	print(result)
	
#调用原函数
sum_num(10, 20)  #在装饰器的底层,相当于调用inner()

栗子4:进阶:不定长参数的装饰器

def logging(fn):
	def inner(*args, **kwargs):
		#输出日 志信息
		print('-----输出日志:正在努力进行加法运算')
		fn(*args, **kwargs)
	return inner

#定义源函数
@logging
def sum_num(*args, **kwargs):
	sum1 = 0
	for i in args:
		sum1 += i
	for value in kwargs.values():
		sum1 += value
	print(sum1)
##调用sum_num函数
sum_num(10,20,30,a=40,b=50)

装饰器-装饰带有返回值的函数for通用装饰器

原函数:

def func(num1,num2):
	result = num1+ num2
	return result
print(func(10,20))

需求:编写一个装饰器,在不改变原函数以及调用方式的基础上,为其添加一个日志功能

# 定义装饰器名称
def logging(fn);
	# ①嵌套
	def inner(num1, num2):
		# 日志输出功能
		print('这是日志信息')
		# ② 引用
		return fn(num1, num2)  # fn = func, fn() → func(10,20) → 30
	# ③返回
	return inner

@logging
def func(num1, num2):
	result = num1 + num2
	return result

print(func(10, 20))  # func(10, 20) 调用inner(10, 20)

通用装饰器(重点)

重点:通用装饰器=>既可以装饰器带有参数的函数,也可以用于装饰带有返回值的函数
总结口诀:通用装饰器五步走=>①有嵌套②有引用③有不定长参数④有返回值⑤返回内层函数的地址

以后都写通用装饰器

def logging(fn):
	def inner(*args, **kwargs):
		print('日志')
		return fn(*args, **kwargs)
	return inner

@logging
def sub_num(num1, num2):
	return num1 - num2

print(sub_num(20, 10))

sum是加 sub是减

装饰器传参(带有参数的通用装饰器)

再加一层嵌套用于传参

# 装饰器传参(带有参数的装饰器)
def decoration(flag):
    def logging(fn):
        def inner(*args, **kwargs):
            # 判断是加法还是减法运算
            if flag == '+':
                print('---正在努力加法运算---')
            elif flag == '-':
                print('---正在努力减法运算---')
            return fn(*args, **kwargs)
        return inner
    return logging

@decoration('+')
def sum_num(a, b):
    return a + b

print(sum_num(1, 2))

@decoration('-')
def sub_num(a, b):
    return a - b

print(sub_num(1, 2))

类装饰器

装饰器大多数都是通过函数来装饰函数,其实也可以通过一个类来装饰函数,这种装饰器就称之为”类装饰器”

# 装饰器大多数都是通过函数来装饰函数,其实也可以通过一个类来装饰函数,这种装饰器就称之为”类装饰器”
# 类装饰器规则:
# 1、必须有一个__init__方法,用于接收要修饰的函数
# 2、必须把这个类转换为可以调用的函数 → __call__方法

# 定义一个类装饰器
class Check():
    def __init__(self, fn):
        self.__fn = fn

    def __call__(self, *args, **kwargs):
        print('请先登录')
        self.__fn()

# 定义一个源函数
@Check
def comment():
    print('发表评论')

# 调用原函数
comment()

一个妙哉的装饰器例题

"""
4. 题干
根据如下说明,编写代码完成相关需求
1、
不带装饰器的基础功能:entry_grade
可以完成『成绩录入功能』
1.1可以重复录入成绩,默认所有输入都是合法的(1~100之间的数)
1.2当录入成绩为0时,结束成绩的录入
1.3将录入的成绩保存在列表中并返回给外界,例如:[90, 80, 50, 70]

2、
选择课程装饰器:choose_course
为『成绩录入功能』新增选择课程的拓展功能,达到可以录入不同学科的成绩
2.1可以重复输入要录入的学科名,然后就可以进入该门学科的『成绩录入功能』,录入结束后,可以进入下一门学科成绩录入
2.2当输入学科名为q时,结束所有录入工作
2.3将学科成绩保存在字典中并返回给外界,例如:{'math': [90, 80, 50, 70], 'english': [70, 50, 55, 90]}

3、
处理成绩装饰器:deal_fail
可以将所有录入的成绩按60分为分水岭,转换为 "通过" | "不通过"进行存储
3.1,如果只对原功能装饰,结果还为list返回给外界,例如:["通过", "通过", "不通过", "通过"]
3.2,如果对已被选择课程装饰器装饰了的原功能再装饰,结果就为dict返回给外界,
例如:{'math': ["通过", "通过", "不通过", "通过"],'english': ["通过", "不通过", "不通过", "通过"]}
"""


def choose_course(func):
    def inner(*args, **kwargs):
        course_dict = {}
        while True:
            course = input("请输入要录入的学科名:")
            if course == "q":
                break
            course_dict[course] = func(*args, **kwargs)
        return course_dict
    return inner

def deal_fail(func):
    def inner(*args, **kwargs):
        result = func(*args, **kwargs)
        return ["通过" if grade >= 60 else "不通过" for grade in result]
    return inner

@choose_course
@deal_fail
def entry_grade():
    result = []
    while True:
        grade = int(input("请输入成绩:"))
        if grade == 0:
            break
        result.append(grade)
    return result

print(entry_grade())