Python学习Day8(下) ---- 闭包与装饰器

1. __ call __方法

python中一切皆对象,一个方法可以调用,称可调用对象。
一个类可以变成一个可调用对象,只要实现了__ call __方法。

class Person(object):
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

    def __call__(self, friend):
        print('My name is %s...' % self.name)
        print('My friend is %s...' % friend)

class Fib(object):
    def __init__(self):
        pass
    def __call__(self,num):
        a, b = 0, 1
        self.l = []

        for i in range(num):
            self.l.append(a)
            a, b = b, a+b
        return self.l
    def __str__(self):
        return str(self.l)

p = Person('Bob', 'male')
p('Tim')
f = Fib()
print(f(10))

>>> My name is Bob...
>>> My friend is Tim...
>>> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

函数内部再定义一个函数,并且内部函数用到了外部函数作用域里的变量(enclosing),那么将这个内部函数以及用到的外部函数内的变量一起称为闭包(Closure)。装饰器(decorator)接受一个callable对象(可以是函数或者实现了call方法的类)作为参数,并返回一个callable对象,它经常用于有切面需求的场景,比如:插入日志、性能测试(函数执行时间统计)、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用

2. 函数

  • 函数定义
    在讨论装饰器之前先谈谈函数,因为装饰器的本质终究还是函数。
def fun(n):
    print(n)

f = fun
print(f)
f(1)

>>> <function fun at 0x002DF7C8>
>>> 1

上面例子中声明了一个函数,名字叫fun,fun指向了一个函数function对象,地址在0x002DF7C8,调用函数只需给函数名加上括号(有参数就传参)

  • 函数作为返回值
    一直有提到的,python中一切皆对象,函数也可以作为其他函数的返回值
def fun():
    return 'this is fun'

def bar():
    return fun

print(bar()())
print(fun())

b = bar()
print(b)
print(fun)

>>> this is fun
>>> this is fun
>>> <function fun at 0x0074F7C8>
>>> <function fun at 0x0074F7C8>

bar()的返回值是函数对象,可以对返回值继续调用,调用bar()等价于fun,两者指向同一内存地址的对象

  • 函数作为参数
def fun():
    return 'this is fun'

def bar(a):
    return a()

print(bar(fun))

>>> this is fun

bar接收一个参数,参数是可调用的对象,返回该对象的调用执行结果,例子中a和fun两个变量名都指向了同一个函数对象,执行a()相当于执行了fun()

3. 闭包

def outer(x):
    def inner():
        print(x)

    return inner

closure = outer(5)
closure()

函数内部再定义一个函数,并且内部函数用到了外部函数作用域里的变量(enclosing),那么将这个内部函数以及用到的外部函数内的变量一起称为闭包(Closure)

4. 装饰器

经过上面对函数和闭包概念的了解,现在开始切入正题----装饰器

装饰器(decorator)接受一个callable对象(可以是函数或者实现了call方法的类)作为参数,并返回一个callable对象

它经常用于有切面需求的场景,比如:插入日志、性能测试(函数执行时间统计)、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。

4.1 修饰无参的函数

举个简单的例子,假如执行下面的业务:

def work():
    print('工作中....')

为了方便维护和调试,需要加上日志:

import datetime

def work():
    print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), '开始记录日志;')
    print('工作中....')
    print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), '结束记录日志。')

work()

>>> 2019-07-12 18:29:39 开始记录日志;
>>> 工作中....
>>> 2019-07-12 18:29:39 结束记录日志。

要实现日志记录的功能就要侵入代码去修改,如果有很多个这样的函数需要加日志,name就会很麻烦,这时候装饰器就能派上用场了

import datetime

def outer(fun):
    def inner():
        print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), '开始记录日志;')
        fun()
        print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), '结束记录日志。')
    return inner

def work():
    print('工作中....')

w = outer(work)
w()

这样做没有修改原来work()里面的任何代码逻辑,其中outer函数就是一个装饰器,它接收的参数是一个函数,返回的是一个新函数的闭包,装饰器本质是一个函数,而闭包至少有里函数和外函数,那么一般写一个装饰器至少嵌套3个函数。

乍一看装饰器很麻烦,但装饰器一旦定义好,很多相同需求的函数都能使用,python中还提供了语法糖@,用在函数定义处:

@outer
def work():
    print('工作中....')

work()

>>> 2019-07-12 18:39:25 开始记录日志;
>>> 工作中....
>>> 2019-07-12 18:39:25 结束记录日志。

这样一看简洁、优雅了许多,

4.2 修饰的函数带参数
def login_required(func):
    def inner(*args, **kwargs):
        if func.__name__ == 'f1':
            print(func.__name__, ' 权限验证成功')
            func(*args, **kwargs)
        else:
            print(func.__name__, ' 权限验证失败')
    return inner

@login_required
def f1(a, b):
    print('function f1, args: ', a, ',', b)

f1(10, 20)

>>> f1  权限验证成功
>>> function f1, args:  10 , 20

Python中的* args和** kwargs可以匹配任意长度的位置参数或关键字参数,可以适应不同函数参数不相同的情况。

4.3 修饰的函数有返回值
def login_required(func):
    def inner(*args, **kwargs):
        if func.__name__ == 'f1':
            print(func.__name__, ' 权限验证成功')
            func(*args, **kwargs)
        else:
            print(func.__name__, ' 权限验证失败')
    return inner

@login_required
def f1(a, b, c):
    print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
    return 'hello, world'

res = f1(10, 20, 30)
print(res)

>>> f1  权限验证成功
>>> function f1, args: a=10, b=20, c=30
>>> None

输出结果中多了一个None,原因是在调用f1(10, 20, 30)时,实际是调用inner(10, 20, 30),然后执行inner闭包的函数体,在执行到func(* args, ** kwargs)后,没有接收原f1函数体的返回值。当inner闭包执行完毕,Python解释器也没有发现有return语句,就默认返回None

在inner中接收func函数的返回值,然后return返回它,本示例中装饰器的inner执行完func(* args, ** kwargs)后没有其它代码了,所以可以直接修改为return func(* args, ** kwargs),如果还有其它逻辑,则用变量保存func的返回值res = func(* args, ** kwargs),inner最后一行返回return res

def login_required(func):
    def inner(*args, **kwargs):
        if func.__name__ == 'f1':
            print(func.__name__, ' 权限验证成功')
            return func(*args, **kwargs)
        else:
            print(func.__name__, ' 权限验证失败')
    return inner

@login_required
def f1(a, b, c):
    print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
    return 'hello, world'

res = f1(10, 20, 30)
print(res)

>>> f1  权限验证成功
>>> function f1, args: a=10, b=20, c=30
>>> hello, world
4.4 装饰器带参数

假如装饰器带参数,那么把带参数的装饰器看做一个函数,他返回的是一个无参装饰器,即在一个装饰器外面再加上一层函数

def logging(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print('[日志级别 {}]: 被装饰的函数名是 {}'.format(level, func.__name__))
            return func(*args, **kwargs)
        return wrapper
    return decorator

@logging('DEBUG')  # 等价于 f1 = logging('DEBUG')(f1) ,即先执行loggin('DEBUG'),返回decorator引用(真正的装饰器),再用decorator装饰f1,返回wrapper
def f1(a, b, c):
    print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
    return 'hello, world'

@logging('INFO')
def f2():
    print('function f2...')

res = f1(10, 20, 30)
print(res)
f2()

>>> [日志级别 DEBUG]: 被装饰的函数名是 f1
>>> function f1, args: a=10, b=20, c=30
>>> hello, world
>>> [日志级别 INFO]: 被装饰的函数名是 f2
>>> function f2...
4.5 @wraps

Python装饰器(decorator)在实现的时候,被装饰后的函数其实已经是另外一个函数了(函数名等函数属性会发生改变),为了不影响,Python的functools包中提供了一个叫wraps的decorator来消除这样的副作用。写一个decorator的时候,最好在实现之前加上functools的wrap,它能保留原有函数的名称和docstring。

  • 不加@wraps
def my_decorator(func):
    def wrapper(*args, **kwargs):
        """这是wrapper的文档"""
        print('calling start')
        func(*args, **kwargs)
        print('calling end')
    return wrapper

@my_decorator   
def exam():
    """这是exam的文档"""
    print('example....')

print(exam.__name__)
print(exam.__doc__)

>>> wrapper
>>> 这是wrapper的文档
  • 加@wraps
from functools import wraps
def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """这是wrapper的文档"""
        print('calling start')
        func(*args, **kwargs)
        print('calling end')
    return wrapper

@my_decorator   
def exam():
    """这是exam的文档"""
    print('example....')

print(exam.__name__)
print(exam.__doc__)

>>> exam
>>> 这是exam的文档
4.6 基于类实现装饰器

实现了call方法,那么类实例化后的对象就是callable,即拥有了被直接调用的能力。装饰器接受一个callable对象作为参数,并返回一个callable对象,那么我们可以让类的构造函数init ()接受一个函数,然后重载call ()并返回一个函数,也可以达到装饰器函数的效果:

class logging(object):
    def __init__(self, func):
        self._func = func

    def __call__(self, *args, **kwargs):
        print('[DEBUG]: 被装饰的函数名是 {}'.format(self._func.__name__))
        return self._func(*args, **kwargs)

@logging
def f1(a, b, c):
    """This is f1 function"""
    print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
    return 'hello, world'

res = f1(10, 20, 30)
print(res)

>>> [DEBUG]: 被装饰的函数名是 f1
>>> function f1, args: a=10, b=20, c=30
>>> hello, world

如果类装饰器带参数,那么就需要在__ call __方法中返回闭包

class logging(object):
    def __init__(self, level='INFO'):
        self._level = level

    def __call__(self, func):  # 接受函数
        def wrapper(*args, **kwargs):
            print('[日志级别 {}]: 被装饰的函数名是 {}'.format(self._level, func.__name__))
            return func(*args, **kwargs)
        return wrapper  # 返回闭包

@logging('DEBUG')
def f1(a, b, c):
    """This is f1 function"""
    print('function f1, args: a={}, b={}, c={}'.format(a, b, c))
    return 'hello, world'

res = f1(10, 20, 30)
print(res)

很多笔记直接抄的课件,但也相当于复习了一边,加深了印象,改天再找些实战练习一下

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

推荐阅读更多精彩内容

  • 一、创建对象流程 二、对象内存布局: 对象头 存储对象自身的运行时数据 类型指针:指向此类的元数据的指针,确定这个...
    Lucksheep阅读 144评论 0 0
  • 1、《精要主义》说了什么? 《精要主义》致力于解决人们被大量的选择、信息包围,没有目标和重点等事实,并提出了一系列...
    应祺辰2015阅读 2,057评论 0 2
  • 每个人都是天生舞者。 最开始接触“灵性舞蹈”是源于参加了一个好玩的workshop,当时一看到跳舞,就按捺不住内心...
    丁少小蕾Melody阅读 1,109评论 2 11
  • 去苏州的时候,突然想吃陕西套餐,便跟叶叶找了家陕西馆子。招呼的小哥很是热情,看了他们的菜单,套餐都是什么凉皮,肉夹...
    木梓楠阅读 1,106评论 0 0
  • 硕大的地方有少许人飘荡 球场前也少了运动员 不同于往日的声势浩大 灯光下 暴露了人的本性 他们都是为同一个目的才相...
    书新一阅读 410评论 3 2