Python 简明教程 --- 22,Python 闭包与装饰器

当你选择了一种语言,意味着你还选择了一组技术、一个社区。

目录

本节我们来介绍闭包装饰器

闭包与装饰器是函数的高级用法,其实在介绍完Python 函数我们就可以介绍本节的内容,但由于Python中的也可以用来实现装饰器,所以我们等到介绍完了Python 类再来统一介绍闭包与装饰器。

装饰器使用的是闭包的特性,我们先来介绍闭包,再来介绍装饰器。

1,什么是闭包

Python 的函数内部还允许嵌套函数,也就是一个函数中还定义了另一个函数。如下:

def fun_1():

    def fun_2():
        return 'hello'

    s = fun_2()

    return s

s = fun_1()
print(s)    # 'hello'

在上面的代码中,我们在函数fun_1 的内部又定义了一个函数fun_2,这就是函数嵌套

我们在学习函数的时候,还知道,Python 函数可以作为函数参数函数返回值

因此,我们可以将上面代码中的函数fun_2作为函数fun_1的返回值,如下:

def fun_1():
    def fun_2():
        return 'hello'

    return fun_2

此时,函数fun_1 返回了一个函数,我们这样使用fun_1

fun = fun_1()    # fun 是一个函数
s = fun()        # 调用函数 fun
print(s)         # s 就是 'hello'

我们再来改进函数fun_1,如下:

def fun_1(s):
    s1 = 'hello ' + s

    def fun_2():
        return s1 

    return fun_2

上面的代码中,内部函数fun_2返回了变量s1,而s1是函数fun_2外部变量,这种内部函数能够使用外部变量,并且内部函数作为外部函数返回值,就是闭包

编写闭包时都有一定的套路,也就是,闭包需要有一个外部函数包含一个内部函数,并且外部函数的返回值是内部函数

2,用闭包实现一个计数器

我们来实现一个计数器的功能,先写一个框架,如下:

def counter():

    # 定义内部函数
    def add_one():
        pass
    
    # 返回内部函数
    return add_one

再来实现计数的功能,如下:

def counter():
    # 用于计数
    l = [0]
    
    # 定义内部函数
    def add_one():
        l[0] += 1
        return l[0] # 返回数字
    
    # 返回内部函数
    return add_one

上面的代码中,我们使用了一个列表l[0]来记录累加数,在内部函数add_one中对l[0]进行累加。

我们这样使用这个计数器:

c = counter()

print(c())   # 1
print(c())   # 2
print(c())   # 3

我们还可以使这个计数器能够设置累加的初始值,就是为counter 函数设置一个参数,如下:

def counter(start):
    l = [start]

    def add_one():
        l[0] += 1
        return l[0] 
        
    return add_one

这样我们就可以使用counter 来生成不同的累加器(从不同的初始值开始累加)。我们这样使用该计数器:

c1 = counter(1)   # c1 从 1 开始累加
print(c1())       # 2
print(c1())       # 3
print(c1())       # 4

c5 = counter(5)   # c5 从 5 开始累加
print(c5())       # 6
print(c5())       # 7
print(c5())       # 8

c11 开始累加,c55 开始累加,两个互不干扰。

3,什么是装饰器

装饰器闭包的一种进阶应用。装饰器从字面上理解就是用来装饰包装的。装饰器一般用来在不修改函数内部代码的情况下,为一个函数添加额外的新功能。

装饰器虽然功能强大,但也不是万能的,它也有自己适用场景:

  • 缓存
  • 身份认证
  • 记录函数运行时间
  • 输入的合理性判断

比如,我们有一个函数,如下:

def hello():
    print('hello world.')

如果我们想计算这个函数的运行时间,最直接的想法就是修改该函数,如下:

import time

def hello():
    s = time.time()

    print('hello world.')

    e = time.time()
    print('fun:%s time used:%s' % (hello.__name__. e - s))

# 调用函数
hello()

其中,time 模块是Python 中的内置模块,用于时间相关计算。

每个函数都有一个__name__ 属性,其值为函数的名字。不管我们是直接查看一个函数的__name__ 属性,还是将一个函数赋值给一个变量后,再查看这个变量的__name__ 属性,它们的值都是一样的(都是原来函数的名字):

print(hello.__name__)  # hello
f = hello              # 调用 f() 和 hello() 的效果是一样的
print(f.__name__)      # hello

但是,如果我们要为很多的函数添加这样的功能,要是都使用这种办法,那会相当的麻烦,这时候使用装饰器就非常的合适。

最简单的装饰器

装饰器应用的就是闭包的特性,所以编写装饰器的套路与闭包是一样的,就是有一个外部函数和一个内部函数,外部函数的返回值是内部函数。

我们先编写一个框架:

def timer(func):
    def wrapper():
        pass
      
    return wrapper

再来实现计时功能:

import time

def timer(func):
    def wrapper():
        s = time.time()
        ret = func()
        e = time.time()
        print('fun:%s time used:%s' % (func.__name__, e - s))
        
        return ret

    return wrapper

def hello():
    print('hello world.')

该装饰器的名字是timer,其接受一个函数类型的参数funcfunc 就是要修饰的函数。

func 的函数原型要与内部函数wrapper 的原型一致(这是固定的写法),即函数参数相同,函数返回值也相同。

英文 wrapper 就是装饰的意思。

其实timer 就是一个高阶函数,其参数是一个函数类型,返回值也是一个函数。我们可以这样使用timer 装饰器:

hello = timer(hello)
hello()

以上代码中,hello 函数作为参数传递给了timer 装饰器,返回结果用hello 变量接收,最后调用hello()。这就是装饰器的原本用法。

只不过,Python 提供了一种语法糖,使得装饰器的使用方法更加简单优雅。如下:

@timer
def hello():
    print('hello world.')

hello()

直接在原函数hello 的上方写一个语法糖@timer,其实这个作用就相当于hello = timer(hello)

用类实现装饰器

在上面的代码中,是用函数(也就是timer 函数)来实现的装饰器,我们也可以用来实现装饰器。

实现装饰器,主要依赖的是__init__ 方法和__call__ 方法。

我们知道,实现__call__ 方法的类,其对象可以像函数一样被调用。

用类来实现timer 装饰器,如下:

import time

class timer:
    def __init__(self, func):
        self.func = func

    def __call__(self):
        s = time.time()
        ret = self.func()
        e = time.time()
        print('fun:%s time used:%s' % (self.func.__name__, e - s))

        return ret

@timer
def hello():
    print('hello world.')

print(hello())

其中,构造方法__init__接收一个函数类型的参数func,然后,__call__方法就相当于wrapper 函数。

用类实现的装饰器的使用方法,与用函数实现的装饰器的使用方法是一样的。

4,被修饰的函数带有参数

如果hello 函数带有参数,如下:

def hello(s):
    print('hello %s.' % s)

那么装饰器应该像下面这样:

import time

def timer(func):
    def wrapper(args):
        s = time.time()

        ret = func(args)

        e = time.time()
        print('fun:%s time used:%s' % (func.__name__, e - s))
        
        return ret

    return wrapper

@timer
def hello(s):
    print('hello %s.' % s)

hello('python')

timer 函数的参数依然是要被修饰的函数,wrapper 函数的原型与hello 函数保持一致。

用类来实现,如下:

import time

class timer:
    def __init__(self, func):
        self.func = func

    def __call__(self, args):
        s = time.time()
        ret = self.func(args)
        e = time.time()
        print('fun:%s time used:%s' % (self.func.__name__, e - s))

        return ret

@timer
def hello(s):
    print('hello %s.' % s)

print(hello('python'))

不定长参数装饰器

如果hello 函数的参数是不定长的,timer 应该是如下这样:

import time

def timer(func):
    def wrapper(*args, **kw):
        s = time.time()

        ret = func(*args, **kw)

        e = time.time()
        print('fun:%s time used:%s' % (func.__name__, e - s))
        
        return ret

    return wrapper

@timer
def hello(s1, s2): # 带有两个参数
    print('hello %s %s.' % (s1, s2))

@timer
def hello_java():  # 没有参数
    print('hello java.')

hello('python2', 'python3')
hello_java()

这样的装饰器timer,可以修饰带有任意参数的函数。

用类来实现,如下:

import time

class timer:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kw):
        s = time.time()
        ret = self.func(*args, **kw)
        e = time.time()
        print('fun:%s time used:%s' % (self.func.__name__, e - s))

        return ret

@timer
def hello(s1, s2): # 带有两个参数
    print('hello %s %s.' % (s1, s2))

@timer
def hello_java():  # 没有参数
    print('hello java.')

hello('python2', 'python3')
hello_java()

5,装饰器带有参数

如果装饰器也需要带有参数,那么则需要在原来的timer 函数的外层再嵌套一层函数TimerTimer 也带有参数,如下:

import time

def Timer(flag):
    def timer(func):
        def wrapper(*args, **kw):
            s = time.time()
            ret = func(*args, **kw)
            e = time.time()
            print('flag:%s fun:%s time used:%s' % (flag, func.__name__, e - s))
            
            return ret

        return wrapper

    return timer

@Timer(1)
def hello(s1, s2): # 带有两个参数
    print('hello %s %s.' % (s1, s2))

@Timer(2)
def hello_java():  # 没有参数
    print('hello java.')

hello('python2', 'python3')
hello_java()

从上面的代码中可以看到,timer 的结构没有改变,只是在wrapper 的内部使用了flag 变量,然后timer 的外层多了一层TimerTimer 的返回值是timer,我们最终使用的装饰器是Timer

我们通过函数.__name__ 来查看函数的__name__ 值:

print(hello.__name__)       # wrapper
print(hello_java.__name__)  # wrapper

可以发现hellohello_java__name__ 值都是wrapper(即内部函数wrapper 的名字),而不是hellohello_java,这并不符合我们的需要,因为我们的初衷只是想增加hellohello_java的功能,但并不想改变它们的函数名字。

6,使用 @functools.wraps

我们可以使用functools模块的wraps装饰器来修饰wrapper 函数,以解决这个问题,如下:

import time
import functools

def Timer(flag):
    def timer(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            s = time.time()
            ret = func(*args, **kw)
            e = time.time()
            print('flag:%s fun:%s time used:%s' % (flag, func.__name__, e - s))
            
            return ret

        return wrapper

    return timer

@Timer(1)
def hello(s1, s2): # 带有两个参数
    print('hello %s %s.' % (s1, s2))

@Timer(2)
def hello_java():  # 没有参数
    print('hello java.')

此时,再查看hellohello_java__name__值,分别是hellohello_java

7,装饰器可以叠加使用

装饰器也可以叠加使用,如下:

@decorator1
@decorator2
@decorator3
def func():
    ...

上面代码的所用相当于:

decorator1(decorator2(decorator3(func)))

8,一个较通用的装饰器模板

编写装饰器有一定的套路,根据上文的介绍,我们可以归纳出一个较通用的装饰器模板:

def func_name(func_args):

    def decorator(func):
    
        @functools.wraps(func)
        def wrapper(*args, **kw):
            # 在这里可以使用func_args,*args,**kw
            # 逻辑处理
            ...
            ret = func(*args, **kw)
            # 逻辑处理
            ...
            
            return ret

        return wrapper

    return decorator


# 使用装饰器 func_name
@func_name(func_args)
def func_a(*args, **kw):
    pass

在上面的模板中:

  • func_name 是装饰器的名字,该装饰器可以接收参数 func_args
  • 内部函数 decorator 的参数 func,是一个函数类型的参数,就是将来要修饰的函数
  • func 的参数列表可以是任意的,因为我们使用的是*args, **kw
  • 内部函数wrapper 的原型(即参数与返回值)要与 被修饰的函数func 保持统一
  • @functools.wraps 的作用是保留被装饰的原函数的一些元信息(比如__name__ 属性)

与装饰器相关的模块有functoolswrapt,可以使用这两个模块来优化完善你写的装饰器,感兴趣的小伙伴可以自己拓展学习。

(完。)


推荐阅读:

Python 简明教程 ---17,Python 模块与包
Python 简明教程 ---18,Python 面向对象
Python 简明教程 ---19,Python 类与对象
Python 简明教程 ---20,Python 类中的属性与方法
Python 简明教程 ---21,Python 继承与多态

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容