Python高级第五天

闭包和装饰器

一、魔法方法之__call__

在Python中,函数其实是一个对象,
所有的函数都是可调用对象,
一个类实例也可以变成一个可调用对象,只需要实现一个特殊方法__call__()
示例:

# 斐波那契数列
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)


f = Fib()
print(f(10))

二、闭包

Python中函数也是对象,允许把函数本身作为参数传入另一个函数,还可以把函数作为结果值返回,例如装饰器。

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

示例:

# 1. 错误的用法
def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i*i
        fs.append(f)
    return fs

f1, f2, f3 = count()

# f1()、f2()和f3()的输出结果都是9,原因是调用这三个函数时,闭包中引用的外部函数中变量i的值已经变成3

# 2. 正确的用法
def count():
    def f(j):
        return lambda: j * j

    fs = []
    for i in range(1, 4):
        fs.append(f(i))  # f(i)立刻被执行,因此i的当前值被传入闭包lambda: j * j
    return fs


f1, f2, f3 = count()
print(f1())  # 输出1
print(f2())  # 输出4
print(f3())  # 输出9

注意: 闭包中不要引用外部函数中任何循环变量或后续会发生变化的变量

三、装饰器

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

1. 被装饰的函数无参数

没有使用装饰器之前:

def f1():
    print('function f1...')

def f2():
    print('function f1...')

f1()  # 输出function f1...
f2()  # 输出function f2...

创建装饰器,接收函数参数,返回一个闭包函数inner:

def login_required(func):
    def inner():  # inner是一个闭包,它使用了外部函数的变量func,即传入的原函数引用f1、f2...
        if func.__name__ == 'f1':  # 这里是权限验证的逻辑判断,此处简化为只能调用f1
            print(func.__name__, ' 权限验证成功')
            func()  # 执行原函数,相当于f1()或f2()...
        else:
            print(func.__name__, ' 权限验证失败')
    return inner

使用装饰器:

def f1():
    print('function f1...')

def f2():
    print('function f1...')

new_f1 = login_required(f1)  # 将f1传入装饰器,返回inner引用,并赋值给新的变量new_f1
new_f1()  # 执行函数,即执行inner(),这个闭包中使用的func变量指向原f1函数体

new_f2 = login_required(f2)  # 将f2传入装饰器,返回inner引用,并赋值给新的变量new_f2
new_f2()  # 执行函数,即执行inner(),func变量指向原f2,所以它不会通过权限验证,即不会执行func()

# 输出结果:
f1  权限验证成功
function f1...
f2  权限验证失败

上面使用装饰器有个问题,就是用户原来是调用f1()、f2()... ,现在你让他们调用new_f1()、new_f2()... , 这样肯定不行,所以需要修改如下:

f1 = login_required(f1)  # 将f1引用传入装饰器,此时func指向了原f1函数体。返回inner引用,并赋值给f1,即现在是func指向原函数体,而f1重新指向了返回的inner闭包
f1()  # 执行函数,即执行inner(),这个闭包中使用的func变量指向原f1函数体

上述两个步骤可以用@Python语法糖简写为:

# 1. 定义时
@login_required
def f1():
    print('function f1...')

# 2. 调用时
f1()
2. 被装饰的函数有参数

示例:

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

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

f1(10)

# 输出结果:
f1  权限验证成功
function f1, args: a= 10

调用f1(10)此时实际调用的是inner(10)

被装饰的函数有多个参数时:

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

使用Python中的*args和**kwargs来匹配任意长度的位置参数或关键字参数

3. 被装饰的函数有返回值

示例:

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. 装饰器带参数

像Flask的@route('/index')就是带参数的,其实route只是一个函数,它返回真正的装饰器,即在原来的装饰器外面再加一层函数.
示例:

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...
5. 使用@wraps

示例:

from functools import wraps

def logging(level='INFO'):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            """print log before a function."""
            print('[日志级别 {}]: 被装饰的函数名是 {}'.format(level, func.__name__))
            return func(*args, **kwargs)
        return wrapper
    return decorator

@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)

print('正确的函数签名:', f1.__name__)
print('正确的函数文档:', f1.__doc__)

# 输出结果:
[日志级别 DEBUG]: 被装饰的函数名是 f1
function f1, args: a=10, b=20, c=30
hello, world
正确的函数签名: f1
正确的函数文档: This is f1 function

如果不加@wraps(func),调用f1(10, 20, 30),实际是调用装饰器中的wrapper(),所以打印出来的函数签名和文档都是wrapper的,所以使用functools模块的wraps装饰器解决这个问题。

6. 多个装饰器装饰同一个函数

示例:

# 装饰器1
def makeBold(func):
    print('这是加粗装饰器')

    def blod_wrapped():
        print('---1---')
        return '<b>' + func() + '</b>'

    return blod_wrapped


# 装饰器2
def makeItalic(func):
    print('这是斜体装饰器')

    def italic_wrapped():
        print('---2---')
        return '<i>' + func() + '</i>'

    return italic_wrapped


@makeBold
@makeItalic
def test():
    print('---3---')
    return 'Hello, world'


res = test()
print(res)

# 输出结果:
这是斜体装饰器  # 这在调用res = test()之前就会输出
这是加粗装饰器  # 这在调用res = test()之前就会输出
---1---
---2---
---3---
<b><i>Hello, world</i></b>

注意:
输出结果中,多个装饰器的顺序,包装时,是从下往上的,
调用时,相当于拆包装,肯定是从最外层开始拆.

7. 基于类实现的装饰器

只要类实现了call方法,那么类实例化后的对象就是callable,即拥有了被直接调用的能力:

class Test():
    def __call__(self):
        print('call me!')


t = Test()
t()  # 类实例化后的对象可以直接调用,输出:call me!

装饰器接受一个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

带参数的类装饰器:

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)

# 输出结果:
[日志级别 DEBUG]: 被装饰的函数名是 f1
function f1, args: a=10, b=20, c=30
hello, world

四、装饰器实例

插入日志:
from functools import wraps
import logging

def logged(level, name=None, message=None):
    """
    Add logging to a function. level is the logging
    level, name is the logger name, and message is the
    log message. If name and message aren't specified,
    they default to the function's module and name.
    """
    def decorate(func):
        logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')
        logname = name if name else func.__module__
        log = logging.getLogger(logname)
        logmsg = message if message else func.__name__

        @wraps(func)
        def wrapper(*args, **kwargs):
            log.log(level, logmsg)
            return func(*args, **kwargs)
        return wrapper
    return decorate

# Example use
@logged(logging.DEBUG)
def add(x, y):
    return x + y

@logged(logging.CRITICAL, 'example')
def spam():
    print('Spam!')

print(add(3, 5))
spam()

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

推荐阅读更多精彩内容