闭包函数与装饰器

闭包函数

  • 闭函数指的是在一个函数内部的函数,即嵌套在函数内部的函数。

  • 包函数指的是内部函数对外层函数(非全局作用域)作用域名字的引用。

  • 闭包函数基于函数对象,可以将函数返回到任意位置调用,但是作用域的关系在定义函数的时候就被确定了,与函数的调用位置无关。如果内嵌函数包含对外部函数作用域中名字的引用,该内嵌函数就是闭包函数。

# -*- coding: utf-8 -*-
def outer_func():
    count = 0

    def inner_func():
        nonlocal count
        count += 1
        return count

    return inner_func

counter = outer_func()
print(counter()) # Output: 1
print(counter()) # Output: 2

在这个例子中,outer_func返回内部函数inner_func,每次调用inner_func时,它会递增count计数器并返回当前计数器的值。由于inner_func与外部函数outer_func的作用域相关联,因此可以保存count的值,即使outer_func已经执行完毕。因此,通过闭包,我们可以在函数之间共享数据和状态信息,这对于编写高效的代码非常有用。

闭包函数的特征

  1. 内部函数可以访问外部函数的变量或者参数;

  2. 外部函数返回内部函数的引用;

  3. 外部函数的变量或者参数在内存中得以保留,供内部函数调用。

在 Python 中,函数是一等公民,意味着函数可以像其他任何对象一样被操作,包括被传递和赋值给变量。这种特性允许变量或参数在内存中得以保留,供内部函数调用。

当外部函数执行结束后,Python 不会立即销毁该函数的局部变量和参数,而是将其保存在与函数名相关联的命名空间中。内部函数在访问这些变量时,实际上是在当前作用域中查找此变量(LEGB规则),如果没有找到,则继续在外层作用域中查找,直到全局作用域。

这种行为称为闭包(Closure),它允许在另一个作用域中捕获外部函数的变量,并通过返回内部函数来保留它们。使用闭包,我们可以将状态和逻辑封装在函数内,可以将代码变得更加简洁和易于理解。

以下是一个简单的示例,说明如何使用函数闭包来保留外部变量:

def outer_function(msg):
    def inner_function():
        print(msg)
    
    return inner_function

# 调用外部函数,返回内部函数
my_func = outer_function("Hello World!")

# 调用内部函数,输出 "Hello World!"
my_func()

在上面的例子中,外部函数 outer_function 接收一个 msg 参数,并返回内部函数 inner_function。当我们调用外部函数并将其结果赋值给变量 my_func 时,msg 参数被保存在内存中,并绑定到 my_func 上。每次调用 my_func 时,实际上是在访问 msg 参数,因此我们可以在内部函数中保留和操作外部参数。

onlocal

onlocal 是 Python 3 引入的关键字,用于在嵌套函数中访问和修改上一级函数中定义的局部变量。如果一个变量在当前函数的作用域中没有定义,但是在上一级函数的作用域中有定义,我们可以使用 nonlocal 关键字将该变量标记为非局部变量,这样就可以在当前函数中引用和修改该变量了。

具体地说,nonlocal 关键字的作用相当于 global 关键字,只不过它是访问上一级函数中的变量,而不是全局变量。如果省略了 nonlocal 或者变量在上一级函数中没有定义,则会抛出 SyntaxError 异常。

例如,下面的代码演示了如何在嵌套函数中使用 nonlocal 关键字:

def outer():
    x = 10
    def inner():
        nonlocal x
        x += 1
        print(x)
    inner()  # 输出 11

outer()

在上面的代码中,inner() 函数中使用了 nonlocal 关键字来访问和修改外层 outer() 函数中定义的变量 x,这样就可以在 inner() 函数中访问和修改 x 变量了。

闭包函数作用

  1. 函数体传参方式之 形参

def index(username):
    print(username)


index('shisbuyu')  # 函数体代码需要什么就可以在形参里写什么
  1. 函数体传参方式之 闭包

def outter():
    name = 'shisbuyu'

    def index():
        print(name)  # name写死了, 调用res() name shisbuyu

    return index


res = outer()
res()

做成闭包函数通过形参传参给外层

def outter(name):
    def index():
        print(name)

    return index


res = outter('shisbuyu')
res()  # shisbuyu
res1 = outter('shisuiyi')
res1()  # shisuiyi

装饰器

装饰器可以在不改动原函数代码的情况下,添加其原本没有的功能。简单点说,就是 修改其它函数的功能的函数 。通过使用装饰器,我们可以让一个函数的功能变的更加强大,还可以让我们的代码更加简短整洁。

装饰器原理阐述:将被装饰的函数当做一个参数传到装饰器中,并且让被装饰的函数名指向装饰器 内部的函数,在装饰器的内部函数再调用被装饰的函数

普通装饰器

方式一:不使用语法糖@符号的调用方法:

# -*- coding: utf-8 -*-

# 原函数
def one():
    print("这是一个平平无奇的函数")


# 定义1个装饰器
def loop(func):
    def wrapper(*args, **kw):
        for i in range(2):
            func(*args, **kw)

    return wrapper
    
res=loop(one)
res()

# 输出
这是一个平平无奇的函数
这是一个平平无奇的函数

方式二:使用语法糖@符号的调用方法:

# -*- coding: utf-8 -*-

# 定义1个装饰器
def loop(func):
    def wrapper(*args, **kw):
        for i in range(2):
            func(*args, **kw)

    return wrapper


@loop
def one():
    print("这是一个平平无奇的函数")


one()

# 输出
这是一个平平无奇的函数
这是一个平平无奇的函数

装饰器装饰类

使用类装饰器的时候,记得要返回被装饰的类调用的结果

def decorator(cls):
    def wrapper(*args, **kwargs):
        print("----装饰器扩展代码1------")
        # 通过类实例化对象
        res = cls(*args, **kwargs)
        print("----装饰器扩展代码2------")
        return res

    return wrapper


@decorator  # MyClass = decorator(MyClass)
class MyClass:
    pass

函数带参数

# -*- coding: utf-8 -*-

def loop(func):
    def wrapper(*args, **kw):
        for i in range(2):
            func(*args, **kw)

    return wrapper


@loop
def one(num):
    for i in range(num):
        print("这是一个平平无奇的函数")


one(2)

#输出
这是一个平平无奇的函数
这是一个平平无奇的函数
这是一个平平无奇的函数
这是一个平平无奇的函数

这段代码定义了一个装饰器函数 loop,它接受一个函数作为参数,并返回一个新的函数 wrapperwrapper 函数会在原始函数之前循环执行两次原函数 func(*args, **kw),其中 *args**kw 表示使用任意数目和类型的位置和关键字参数。@loop 语法意味着将函数 one() 包裹在装饰器中,从而每次调用 one() 函数时,实际上是调用了包含两次循环的新函数 wrapper()

带返回值

# -*- coding: utf-8 -*-

def loop(func):
    def wrapper(*args, **kw):
        ret = func(*args, **kw)
        return ret

    return wrapper


@loop
def one(a, b):
    return a + b


res = one(1, 2)
print(res)

# 当使用@loop对one()函数装饰以后,one指向了wrapper()函数,而wrapper()函数的返回值是ret,所以one()函数的返回值被ret接收了

#输出
3

装饰器接受参数

Python 装饰器可以接受参数,这使得装饰器的功能更加灵活和通用。装饰器接受参数的语法比较简单,只需要在装饰器函数外再嵌套一层函数,用于接收装饰器参数并返回一个装饰器函数即可。

具体地说,如果要给装饰器传递参数,可以按照下面的步骤进行:

  1. 定义一个函数作为装饰器的参数接收函数,该函数返回一个装饰器函数。

  2. 在函数内部定义一个装饰器函数,该函数接收被装饰函数的参数,并根据传入的装饰器参数来实现相应的功能。

  3. 将装饰器函数作为返回值返回到装饰器参数接收函数中。

下面是一个简单的例子,演示了如何给装饰器传递参数:

def repeat(num):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(num):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def hello(name):
    print(f"Hello, {name}!")

hello("world")
  • 最外层参数,接收的是装饰器的参数

  • 第二层参数,接收是被装饰的函数

  • 第三层参数,接收的是被装饰函数的参数

在上面的代码中,我们定义了一个名为 repeat 的函数,它接受一个整数 num 作为参数,并返回一个装饰器函数 decorator,该函数内部定义了一个装饰器函数 wrapper,用于实现重复调用被装饰函数的功能。最后,我们把装饰器函数作为返回值返回到装饰器参数接收函数中,并使用 @repeat(3) 的语法将其应用到 hello() 函数上。

运行上面的代码,输出如下:

Hello, world!
Hello, world!
Hello, world!

从输出结果可以看出,我们成功地给装饰器传递了参数,并且实现了重复调用的功能。

装饰器的副作用

问题:函数/类 在被装饰器装饰了之后,会改变原函数名的指向,无法再通过原函数名去获取函数原有的属性

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2023/5/7 10:39
# @Author  : shisuiyi
# @File    : 装饰器.py
# @Software: win10 Tensorflow1.13.1 python3.9
from functools import wraps


def decorator1(func):
    # 消除装饰器的副作用
    # @wraps(func)
    def wrapper(a, b):
        res = func(a, b)
        return res

    return wrapper


@decorator1
def work(a, b):
    """
    实现两个对象相加的方法
     :param a: 数字1
     :param b: 数字2
     :return: 两个数相加的结果
    """
    res = a + b
    print('a+b的结果为:', res)


# 获取函数的文档字符串注释
print(work.__doc__)
# 获取函数名
print(work.__name__)

# 输出
None
wrapper
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2023/5/7 10:39
# @Author  : shisuiyi
# @File    : 装饰器.py
# @Software: win10 Tensorflow1.13.1 python3.9
from functools import wraps


def decorator1(func):
    # 消除装饰器的副作用
    @wraps(func)
    def wrapper(a, b):
        res = func(a, b)
        return res

    return wrapper


@decorator1
def work(a, b):
    """
    实现两个对象相加的方法
     :param a: 数字1
     :param b: 数字2
     :return: 两个数相加的结果
    """
    res = a + b
    print('a+b的结果为:', res)


# 获取函数的文档字符串注释
print(work.__doc__)
# 获取函数名
print(work.__name__)

# 输出
    实现两个对象相加的方法
     :param a: 数字1
     :param b: 数字2
     :return: 两个数相加的结果
    
work

消除装饰器的副作用:functools.wraps

函数的几个特殊的属性

  • 获取函数名:func.__name__

  • 获取函数的文档字符串注释: func.__doc__

  • @wraps(func) 用于修复被装饰之后的函数的函数名和文档字符串注释信息

多重装饰器

def dec1(func):
    def wrapper(*args, **kwargs):
        print("Calling dec1")
        return func(*args, **kwargs)

    return wrapper


def dec2(param):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print("Calling dec2 with param", param)
            return func(*args, **kwargs)

        return wrapper

    return decorator


def dec3(func):
    def wrapper(*args, **kwargs):
        print("Calling dec3")
        return func(*args, **kwargs)

    return wrapper


@dec1
@dec2("hello")
@dec3
def my_func():
    print("Hello, world!")


my_func()

# 输出
Calling dec1
Calling dec2 with param hello
Calling dec3
Hello, world!

Process finished with exit code 0

首先,最后一个装饰器 dec3 会先被调用,然后将 my_func() 作为参数传递给它,并返回一个新的函数。接下来,dec2("hello") 装饰器会被调用,将 dec3 返回的新函数作为参数传递给它,并返回一个新的函数。最后,dec1 装饰器会被调用,将 dec2 返回的新函数作为参数传递给它,并返回一个新的函数。所以,最终的 my_func() 函数是由 dec1 返回的新函数构成的。

因此,在执行 my_func() 函数时,会先调用 dec1 的包装函数 wrapper,然后调用 dec2 的包装函数 wrapper,最后调用 dec3 的包装函数 wrapper。每次包装函数的执行都会在终端上打印出相应的信息,最后才会打印出 "Hello, world!"。

实际案例

日志装饰器

# -*- coding: utf-8 -*-

import logging
import os

'''
* %(asctime)s   即日志记录时间,精确到毫秒@breif: 
* %(levelname)s 即此条日志级别@param[in]: 
* %(filename)s  即触发日志记录的python文件名@retval: 
* %(funcName)s  即触发日志记录的函数名
* %(lineno)s    即触发日志记录代码的行号
* %(message)s   即这项调用中的参数
'''
if not os.path.exists('Log.log'):
    file = open('Log.log', 'w')

logging.basicConfig(
    filename='Log.log',
    format="%(asctime)s - %(levelname)s - %(filename)s - %(funcName)s - %(lineno)s - %(message)s"
)

'''
* @breif: 日志修饰器,为函数添加日志记录服务
* @param[in]: err -> 发生异常时返回的错误信息
* @retval: 加载日志服务的功能函数
'''


def logger(err):
    def log(func):
        def warp(*args, **kwargs):
            try:
                result = func(*args, **kwargs)
                return result
            except Exception as e:
                logging.error(e)
                return err

        return warp

    return log


@logger('出错了')
def add(a, b):
    return a + b


print(add(1, 2))

#控制台输出
出错了
# Log.log日志显示
2022-12-13 14:11:22,772 - ERROR - 闭包函数.py - warp - 36 - unsupported operand type(s) for +: 'int' and 'str'

自动化测试脚本中处理异常

举个例子,你正在跑一个自动化测试脚本,突然间设备的闹钟响了,而刚好闹钟的界面遮挡住了你要操作的那个按钮,最终导致脚本运行失败了。没办法,你只能关掉闹钟再重新运行一次脚本。

# -*- coding: utf-8 -*-

def test_retry(func):
    def run_case_again(*args, **kwargs):
        try:
            func(*args, **kwargs)
        except:
            # 如闹铃、断网重试按钮弹出、短信通知等导致失败
            if exists(xxxx):
                touch(xxx)
            try:
                func(*args, **kwargs)
                print('用例重新执行成功')
            except:
                print('用例执行失败')
                # 其他你想做的操作,比如停止剩余用例的执行

    return run_case_again


@test_retry
def test():
    wait(xxxx, timeout=15)

实现一个可以统计任意函数执行时间的装饰器

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2023/5/7 10:39
# @Author  : shisuiyi
# @File    : 装饰器.py
# @Software: win10 Tensorflow1.13.1 python3.9
import time


def count_time(func):
    def wrapper():
        strat_time = time.time()  # 获取开始时的时间
        func()  # 运行被装饰的函数
        end_time = time.time()  # 获取结束时的时间
        t_time = end_time - strat_time  # 算出运行总时间
        print('运行总时间%:', t_time)  # 打印运行总时间
    return wrapper


@count_time
def work():
    time.sleep(2)
    print('原来函数的功能代码')


if __name__ == '__main__':
    work()