Python基础:闭包和装饰器

在这里插入图片描述

1. Python中的闭包

闭包‌(Closure)是Python中的一个重要概念,它涉及到函数和变量的作用域以及如何通过函数保留和访问其外部作用域的变量。闭包允许函数记住和访问其词法作用域中的变量,即使函数在其作用域之外执行。这种特性使得闭包成为一种强大的工具,可以用于创建函数工厂、装饰器、回调函数等场景。

闭包的基本概念可以概括为:

  1. 定义‌:闭包是一个函数(或方法)以及与其相关的引用环境(包括外部函数的变量和参数)的组合。这意味着,当内部函数被返回或调用时,它能够访问并可能修改其外部函数的局部变量,即使外部函数已经执行完毕。

  2. 形成‌:闭包的形成依赖于两个关键性质:函数作为一等公民(即函数可以被赋值给变量、作为参数传递或作为返回值)和函数嵌套(即在一个函数内部定义另一个函数)。当内部函数引用了外部函数的变量时,就形成了一个闭包。

  3. 优点‌:闭包的主要优点包括能够保留外层函数的状态信息,这对于创建在多次调用中保持状态的对象非常有用,如装饰器。此外,闭包还可以用于数据封装、保持变量状态、延迟计算等。

  4. 使用场景‌:闭包在Python中的使用场景非常广泛,包括但不限于装饰器(在不修改原有函数代码的情况下增加额外功能)、回调函数(封装了状态的函数作为回调传递给某些操作)、函数工厂(根据输入参数的不同返回不同行为的函数)等。

  5. 注意事项‌:使用闭包时,需要注意循环引用问题以及变量的作用域和生命周期,以避免意外的行为。

通过闭包,我们可以创建更灵活可复用的代码,同时保持数据隐藏封装,提高代码的模块性重用性‌

2. Python 装饰器

Python装饰器是一种强大的语言特性,它允许开发者在不修改原始函数代码的情况下,为函数添加额外的功能。‌

装饰器本质上是一个函数,它接受一个函数作为参数,并返回一个新的函数。这个新函数通常会在执行原始函数之前或之后执行一些额外的操作,从而扩展或修改原始函数的行为。装饰器的语法使用@符号,将装饰器函数放在被装饰函数的定义之前。通过这种方式,装饰器为Python编程提供了极大的灵活性和扩展性。

装饰器的应用场景非常广泛,包括但不限于:

  1. 日志记录‌:可以在函数执行前后打印日志信息,便于调试和错误排查。
  2. 性能分析‌:通过记录函数的执行时间,可以分析函数的性能并进行优化。
  3. 缓存‌:通过缓存函数的结果,避免重复计算,提高执行效率。
  4. 权限控制‌:通过检查用户的权限,控制对某些函数的访问。
  5. 错误处理‌:在装饰器中捕获和处理异常,防止程序崩溃。

装饰器的实现基于几个关键概念,包括闭包、函数嵌套和高阶函数。通过这些技术,装饰器能够在不修改原始函数代码和调用方式的前提下,为其添加新的功能或行为。这使得装饰器成为Python中一种非常有用的工具,能够极大地增强代码的可维护性和可扩展性‌。

3. 闭包实践

网络上介绍 Python 闭包的文章已经很多了,本文将通过解决一个需求问题来了解闭包。

3.1 需求场景

这个需求是这样的,我们需要一直记录自己的学习时间,以分钟为单位。就好比我学习了 2 分钟,就返回 2 ,然后隔了一阵子,我学习了 10 分钟,那么就返回 12 ,像这样把学习时间一直累加下去。

面对这个需求,我们一般都会创建一个全局变量来记录时间,然后用一个方法来新增每次的学习时间,通常都会写成下面这个形式:

time = 0

def insert_time(min):
    time = time + min
    return  time

print(insert_time(2))
print(insert_time(10))

认真想一下,会不会有什么问题呢?

其实,这个在 Python 里面是会报错的。会报如下错误:

UnboundLocalError: local variable 'time' referenced before assignment

那是因为,在 Python 中,如果一个函数使用了和全局变量相同的名字且改变了该变量的值,那么该变量就会变成局部变量,那么就会造成在函数中我们没有进行定义就引用了,所以会报该错误。

如果确实要引用全局变量,并在函数中对它进行修改,该怎么做呢?

3.2 global 关键字使用

我们可以使用 global 关键字,具体修改如下:

time = 0


def insert_time(min):
    global  time
    time = time + min
    return  time

print(insert_time(2))
print(insert_time(10))

输出结果如下:

2
12

3.3 全局变量的缺陷

可是啊,这里使用了全局变量,我们在开发中能尽量避免使用全局变量的就尽量避免使用。因为不同模块,不同函数都可以自由的访问全局变量,可能会造成全局变量的不可预知性。比如程序员甲修改了全局变量 time 的值,然后程序员乙同时也对 time 进行了修改,如果其中有错误,这种错误是很难发现和更正的。

全局变量降低了函数或模块之间的通用性,不同的函数或模块都要依赖于全局变量。同样,全局变量降低了代码的可读性,阅读者可能并不知道调用的某个变量是全局变量。

那有没有更好的方法呢?

3.4 闭包改造

这时候我们使用闭包来解决一下,先直接看代码:

time = 0


def study_time(time):
    def insert_time(min):
        nonlocal  time
        time = time + min
        return time

    return insert_time


f = study_time(time)
print(f(2))
print(time)
print(f(10))
print(time)

输出结果如下:

2
0
12
0

这里最直接的表现就是全局变量 time 至此至终都没有修改过,这里还是用了 nonlocal 关键字,表示在函数或其他作用域中使用外层(非全局)变量。那么上面那段代码具体的运行流程是怎样的。我们可以看下下图:

这种内部函数的局部作用域中可以访问外部函数局部作用域中变量的行为,我们称为: 闭包。更加直接的表达方式就是,当某个函数被当成对象返回时,夹带了外部变量,就形成了一个闭包。k

闭包避免了使用全局变量,此外,闭包允许将函数与其所操作的某些数据(环境)关连起来。而且使用闭包,可以使代码变得更加的优雅。而且下一篇讲到的装饰器,也是基于闭包实现的。

3.5 闭包函数验证

到这里,就会有一个问题了,你说它是闭包就是闭包了?有没有什么办法来验证一下这个函数就是闭包呢?

有的,所有函数都有一个 __closure__ 属性,如果函数是闭包的话,那么它返回的是一个由 cell 组成的元组对象。cell 对象的 cell_contents 属性就是存储在闭包中的变量。

我们打印出来体验一下:

time = 0


def study_time(time):
    def insert_time(min):
        nonlocal  time
        time = time + min
        return time

    return insert_time


f = study_time(time)
print(f.__closure__)
print(f(2))
print(time)
print(f.__closure__[0].cell_contents)
print(f(10))
print(time)
print(f.__closure__[0].cell_contents)

打印的结果为:

(<cell at 0x0000000000410C48: int object at 0x000000001D6AB420>,)
2
0
2
12
0
12

从打印结果可见,传进来的值一直存储在闭包的 cell_contents 中,因此,这也就是闭包的最大特点,可以将父函数的变量与其内部定义的函数绑定。就算生成闭包的父函数已经释放了,闭包仍然存在。

闭包的过程其实好比类(父函数)生成实例(闭包),不同的是父函数只在调用时执行,执行完毕后其环境就会释放,而类则在文件执行时创建,一般程序执行完毕后作用域才释放,因此对一些需要重用的功能且不足以定义为类的行为,使用闭包会比使用类占用更少的资源,且更轻巧灵活。

4. 装饰器实践

上一小节中,我们通过解决一个需求问题来感受了闭包,本文也将一样,通过慢慢演变一个需求,一步一步来了解 Python 装饰器。

4.1 场景介绍

首先有这么一个输出员工打卡信息的函数:

def punch():
    print('昵称:两点水  部门:做鸭事业部 上班打卡成功')


punch()

输出的结果如下:

昵称:两点水  部门:做鸭事业部 上班打卡成功

然后,产品反馈,不行啊,怎么上班打卡没有具体的日期,加上打卡的具体日期吧,这应该很简单,分分钟解决啦。好吧,那就直接添加打印日期的代码吧,如下:

import time


def punch():
    print(time.strftime('%Y-%m-%d', time.localtime(time.time())))
    print('昵称:两点水  部门:做鸭事业部 上班打卡成功')


punch()

输出结果如下:

2018-01-09
昵称:两点水  部门:做鸭事业部 上班打卡成功

4.2 函数式编程改造

这样改是可以,可是这样改是改变了函数的功能结构的,本身这个函数定义的时候就是打印某个员工的信息和提示打卡成功,现在增加打印日期的代码,可能会造成很多代码重复的问题。比如,还有一个地方只需要打印员工信息和打卡成功就行了,不需要日期,那么你又要重写一个函数吗?而且打印当前日期的这个功能方法是经常使用的,是可以作为公共函数给各个模块方法调用的。当然,这都是作为一个整体项目来考虑的。

既然是这样,我们可以使用函数式编程来修改这部分的代码。因为通过之前的学习,我们知道 Python 函数有两个特点,函数也是一个对象,而且函数里可以嵌套函数,那么修改一下代码变成下面这个样子:

import time


def punch():
    print('昵称:两点水  部门:做鸭事业部 上班打卡成功')


def add_time(func):
    print(time.strftime('%Y-%m-%d', time.localtime(time.time())))
    func()


add_time(punch)

输出结果:

2018-01-09
昵称:两点水  部门:做鸭事业部 上班打卡成功

这样是不是发现,这样子就没有改动 punch 方法,而且任何需要用到打印当前日期的函数都可以把函数传进 add_time 就可以了,就比如这样:

import time


def punch():
    print('昵称:两点水  部门:做鸭事业部 上班打卡成功')


def add_time(func):
    print(time.strftime('%Y-%m-%d', time.localtime(time.time())))
    func()


def holiday():
    print('天气太冷,今天放假')


add_time(punch)
add_time(holiday)

打印结果:

2018-01-09
昵称:两点水  部门:做鸭事业部 上班打卡成功
2018-01-09
天气太冷,今天放假

4.3 装饰器改造

使用函数编程是不是很方便,但是,我们每次调用的时候,我们都不得不把原来的函数作为参数传递进去,还能不能有更好的实现方式呢?有的,就是本文要介绍的装饰器,因为装饰器的写法其实跟闭包是差不多的,不过没有了自由变量,那么这里直接给出上面那段代码的装饰器写法,来对比一下,装饰器的写法函数式编程有啥不同。

import time


def decorator(func):
    def punch():
        print(time.strftime('%Y-%m-%d', time.localtime(time.time())))
        func()

    return punch


def punch():
    print('昵称:两点水  部门:做鸭事业部 上班打卡成功')


f = decorator(punch)
f()

输出的结果:

2018-01-09
昵称:两点水  部门:做鸭事业部 上班打卡成功

通过代码,能知道装饰器函数一般做这三件事:

  1. 接收一个函数作为参数
  2. 嵌套一个包装函数, 包装函数会接收原函数的相同参数,并执行原函数,且还会执行附加功能
  3. 返回嵌套函数

4.4 装饰器核心:语法糖

可是,认真一看这代码,这装饰器的写法怎么比函数式编程还麻烦啊。而且看起来比较复杂,甚至有点多此一举的感觉。
那是因为我们还没有用到装饰器的 “语法糖” ,我们看上面的代码可以知道, Python 在引入装饰器 (Decorator) 的时候,没有引入任何新的语法特性,都是基于函数的语法特性。这也就说明了装饰器不是 Python 特有的,而是每个语言通用的一种编程思想。只不过 Python 设计出了 @ 语法糖,让 定义装饰器,把装饰器调用原函数再把结果赋值为原函数的对象名的过程变得更加简单,方便,易操作,所以 Python 装饰器的核心可以说就是它的语法糖。

那么怎么使用它的语法糖呢?很简单,根据上面的写法写完装饰器函数后,直接在原来的函数上加 @ 和装饰器的函数名。如下:

import time


def decorator(func):
    def punch():
        print(time.strftime('%Y-%m-%d', time.localtime(time.time())))
        func()

    return punch

@decorator
def punch():
    print('昵称:两点水  部门:做鸭事业部 上班打卡成功')

punch()

输出结果:

2018-01-09
昵称:两点水  部门:做鸭事业部 上班打卡成功

那么这就很方便了,方便在我们的调用上,比如例子中的,使用了装饰器后,直接在原本的函数上加上装饰器的语法糖就可以了,本函数也无虚任何改变,调用的地方也不需修改。

不过这里一直有个问题,就是输出打卡信息的是固定的,那么我们需要通过参数来传递,装饰器该怎么写呢?装饰器中的函数可以使用 *args 可变参数,可是仅仅使用 *args 是不能完全包括所有参数的情况,比如关键字参数就不能了,为了能兼容关键字参数,我们还需要加上 **kwargs

因此,装饰器的最终形式可以写成这样:

import time


def decorator(func):
    def punch(*args, **kwargs):
        print(time.strftime('%Y-%m-%d', time.localtime(time.time())))
        func(*args, **kwargs)

    return punch


@decorator
def punch(name, department):
    print('昵称:{0}  部门:{1} 上班打卡成功'.format(name, department))


@decorator
def print_args(reason, **kwargs):
    print(reason)
    print(kwargs)


punch('两点水', '做鸭事业部')
print_args('两点水', sex='男', age=99)

输出结果如下:

2018-01-09
昵称:两点水  部门:做鸭事业部 上班打卡成功
2018-01-09
两点水
{'sex': '男', 'age': 99}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

月下独码

你的打赏是我精心创作的动力!

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

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

打赏作者

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

抵扣说明:

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

余额充值