Python中的闭包与装饰器

闭包(Closure)

嵌套函数(nested function)

讲解闭包之前,先介绍一下什么是嵌套函数(nested function):

def print_msg(msg):
# This is the outer enclosing function

    def printer():
# This is the nested function
        print(msg)

    printer()


>>> print_msg("Hello")
Hello

具体是这样实现的:首先调用print_msg函数,传入参数msgHello,该函数调用的是其内嵌套的另一个函数printer,该printer函数使用了非局部变量(non-local)msg,完成打印输出。

闭包的概念

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

这里引用的是维基百科对于闭包的解释。注意这里的两个关键词:自由变量函数,也就是说,闭包本质上是一个嵌套函数,还包括该函数引用的非局部自由变量。自由变量的意思从字面上理解就是:不受(系统)约束的变量,也就是该变量并不会随着外部函数的生命周期结束而被回收。为了方便理解,这里还是通过以上嵌套函数的例子来讲解:在该嵌套函数中,如果最后pring_msg不是调用printer而是返回该函数(在Python中,函数作为一等公民,因此可以直接作为对象返回):

def print_msg(msg):
# This is the outer enclosing function

    def printer():
# This is the nested function
        print(msg)

    return printer

在这里,printer函数就是一个闭包,它包括自由变量msg,并且该自由变量并不会随着print_msg函数的声明周期结束而消失:

>>> another = print_msg("Hello")
>>> another()
Hello

>>> del print_msg
>>> print_msg("Hello")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-7-806baf73d191> in <module>()
----> 1 print_msg('Hello')

NameError: name 'print_msg' is not defined

>>> another()
Hello

通过以上的例子我们可以知道,another一开始已经被定义为一个闭包函数了(相当于printer函数),并且拥有自由变量msg,二者已经绑定在一起了,即使msg离开了创造它的环境print_msg也能存在。当del print_msg后,该函数已经不存在了,然后another仍然能够自由使用msg变量(自由变量)。

如何使用闭包

正如上面的例子,我们可以很容易地定义一个闭包函数。那么,当你定义闭包的时候需要注意什么呢?

  • 首先,必须定义一个嵌套函数(函数内的函数)
  • 其次,该嵌套函数必须引用外包函数的参数
  • 外包函数必须引用其内的嵌套函数

何时定义闭包

那么,闭包有什么好处?
定义闭包可以使得你避免使用全局变量(这在任何语言都是极力避免的,因为全局变量不好控制),并且提供某种形式的数据隐藏,还能够提供一个面向对象的解决方案。
当在类中实现的方法很少(大多数情况下只有一个方法)时,闭包可以提供另一种更优雅的解决方案。但是当属性和方法的数量增加时,最好实现一个类。
这里我们举一个简单的例子来说明使用闭包比使用类更合适。这里我们需要的实现是一个简单的倍乘器:

  1. 使用类实现

这里实现的是一个倍乘器的类(虽然用类实现显得有点大材小用),可以将x扩大n倍。

class make_multiplier_of(object):
    def __init__(self, n):
        self.n = n

    def multiplier(self, x):
        return x * self.n

这里定义两个倍乘器(三倍times3与五倍times5

times3 = make_multiplier_of(3)
times5 = make_multiplier_of(5)
>>> times3.multiplier(4) # 3x4
12
>>> times5.multiplier(4) # 5x4
20
  1. 闭包实现

这里是实现的是一个倍乘器的闭包,功能同上面的类。

def make_multiplier_of(n):
    def multiplier(x):
        return x * n
    return multiplier

定义两个倍乘器(三倍times3与五倍times5

times3 = make_multiplier_of(3)
times5 = make_multiplier_of(5)
>>> times3(4) # 3x4
12
>>> times5(4) # 5X4
20
>>> times5(times3(4)) # 5x3x4
60
  1. partial

当然,这种简单的功能其实使用Python中的partial函数即可实现了。

def multiplier(x):
    return x * n

然后也是定义两个倍乘器:

from functools import partial
times3 = partial(multiplier, n=3)
times5 = partial(multiplier, n=5)

这里我们总结下类的实现与闭包实现的区别。虽然结果是一样的,但是显然类的实现相当繁琐,这里实现一个倍乘器使用类确实是小题大做了,同时,make_multiplier_of函数在执行完毕后,其作用域已经释放,但make_multiplier_of类却不是,它会与它的实例times3times5一直贮存在内存中,而这种占用对于实现该功能后,显得十分没有必要。

修改自由变量

这里我们要注意的是,闭包中引用的自由变量是无法修改的,例如:

def outer_function():
    x = 0
    def inner_function():
        x = 1 # try to modify n
        print(f"Inner function: x = {x}")
    print(f"Before: Outer function: x = {x}")
    inner_function()
    print(f"After: Outer function: x = {x}")
>>> outer_function()

Before: Outer function: x = 0
Inner function: x = 1
After: Outer function: x = 0

这里我们可以看到x变量并没有被修改,也就是内嵌函数inner_function()并不能修改非局部变量x,如果想要修改(通常不建议修改),可以使用nonlocal关键字:

def outer_function():
    x = 0
    def inner_function():
        nonlocal x
        x = 1    # try to modify n
        print(f"Inner function: x = {x}")
    print(f"Before: Outer function: x = {x}")
    inner_function()
    print(f"After: Outer function: x = {x}")
>>> outer_function()

Before: Outer function: x = 0
Inner function: x = 1
After: Outer function: x = 1

装饰器(Decorator)

装饰器介绍

Python中有一个十分有趣的特性,叫做装饰器(Decorator),它可以为某些已经存在的函数(代码)添加某些新的功能,即将该函数添加一些新功能后返回这个添加了新功能的函数。这也称为元编程,因为程序的一部分试图在编译时修改程序的另一部分。它可以在不改变现有程序的大体结构上,为其添加新功能,举个例子:

@new_decorated_function
def original_function(*args, **kwargs):
    pass

简而言之,@new_decorated_function就是将original_function(),并返回新的
original_function = new_decorated_function(original_function)
装饰器图解

实现装饰功能

这里为了更好地理解装饰器到底做了什么,我们举点例子:

def make_pretty(func):
    def inner():
        print("I got decorated")
        func()
    return inner

def ordinary():
    print("I am ordinary")

上面定义了一个简单的闭包函数以及一个普通函数,接下来我们要做的事情就是装饰oridinary()函数

>>> ordinary()
I am ordinary

>>> # let's decorate this ordinary function
>>> pretty = make_pretty(ordinary)
>>> pretty()
I got decorated
I am ordinary

可以看到,原本只能输出I am ordinary的函数,经过装饰后,可以输出I got decorated了!(我这么说好像有点奇怪,你可以理解成原本只能走路的人,突然给你装上了翅膀可以飞了)。
在这里make_pretty()是个装饰器,在下面的赋值语句中:

pretty = make_pretty(ordinary)

ordinary()被装饰后,给予了一个新的名字pretty,通常,我们装饰器做了如下工作:

ordinary = make_pretty(ordinary)

添加装饰器

我们可以简单使用@符号,放置于需要装饰的函数前来装饰该函数。

@make_pretty
def ordinary():
    print("I am ordinary")

等价于:

def ordinary():
    print("I am ordinary")
ordinary = make_pretty(ordinary)

含参装饰器

上述的装饰器十分简单,并且只能适用于没有任何参数的函数,如果我们想要实现的函数含有如下的参数呢?

def divide(a, b):
    return a/b

这是一个简单的除法函数,有两个参数ab,我们知道,当b0的时候该函数会出错。

>>> divide(2,5)
0.4
>>> divide(2,0)
Traceback (most recent call last):
...
ZeroDivisionError: division by zero

现在我们已经实现好了divide函数,但是我们发现并没有对b=0做错误处理,而我们又不想重构代码,这时候我们只需要简单写好一个装饰器,装饰该函数即可。

def smart_divide(func):
   def inner(a,b):
      print("I am going to divide",a,"and",b)
      if b == 0:
         print("Whoops! cannot divide")
         return

      return func(a,b)
   return inner

@smart_divide
def divide(a,b):
    return a/b

这里我们实现了一个装饰器,将原来比较不完美的函数divide()装饰成了一个新的更加完善的函数smart_divide()

>>> divide(2,5)
I am going to divide 2 and 5
0.4

>>> divide(2,0)
I am going to divide 2 and 0
Whoops! cannot divide

在这个例子里,我们装饰了带参数的函数。当然,你可能会注意到,装饰器中的内嵌函数inner()与被装饰函数的参数一样。考虑到这一点,现在我们可以定义一个通用装饰器,从而可以使用任意数量的参数。
Python中,使用的是类似function(*args, **kwargs)的实现。其中,args是位置参数的元组,kwargs是关键字参数的字典。一下的装饰器就是一个例子。

def works_for_all(func):
    def inner(*args, **kwargs):
        print("I can decorate any function")
        return func(*args, **kwargs)
    return inner

链式装饰器

在Python中,我们可以将多个装饰器“链接”起来,也就是,一个函数可以被多个相同或者不同的装饰器装饰,例如:

def star(func):
    def inner(*args, **kwargs):
        print("*" * 30)
        func(*args, **kwargs)
        print("*" * 30)
    return inner

def percent(func):
    def inner(*args, **kwargs):
        print("%" * 30)
        func(*args, **kwargs)
        print("%" * 30)
    return inner

@star
@percent
def printer(msg):
    print(msg)

接下来我们调用printer()函数

>>> printer("Hello")

******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************

上述的语句:

@star
@percent
def printer(msg):
    print(msg)

等价于

def printer(msg):
    print(msg)
printer = star(percent(printer))

从这里我们可以看到,顺序是十分重要的,如果顺序交换的话,将会得到不一样的结果:

@percent
@star
def printer(msg):
    print(msg)

调用printer()函数

>>> printer("Hello")
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
Hello
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

结果反过来了~

[1] Python Closures
[2] Python 的闭包和装饰器
[3] Python Decorators
[4] 理解Python中的闭包

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值