闭包与装饰器

这一篇学习闭包与装饰器,包括前置知识、 闭包、 装饰器

 

一、前置知识


理解闭包和装饰器需要函数相关知识

函数名指向函数对象

>>> def hello():
... return 'hello'
...
>>> hello       # 这是函数名
<function hello at 0x7ff1db09f488 >
>>> hello()     # 这是执行函数
'hello'
>>> p = hello   # 可以给函数绑定个新的名字p
>>> p()
'hello'         # 然后执行函数

函数也是对象,函数名是指向函数对象的名字

函数可以嵌套

函数里面可以内嵌函数

// hello_word里面定义hello函数
>>> def hello_word():  
...     def hello():
...         return 'hello'
...     return hello() + 'word'
>>> hello_word()       # 输出'helloword'

也可以返回一个函数对象

// 执行一个函数返回另一个函数
>>> def hello_word(): ... def hello(): ... return 'helloword' ... return hello ... >>> hello_word # 函数名 <function hello_word at 0x7f049e2ffe18 > >>> hello_word() # 执行函数,返回内部的hello函数名(未执行) <function hello_word. < locals > .hello at 0x7f049e257488 > >>> hello_word()() # 相当于执行内层的hello函数 'helloword'

注:可以返回的是未执行的函数

高阶函数

可以把函数作为参数传给另一个函数

def inner(x, y):
    return x + y

def outer(func, x, y):  # outer函数接收一个函数,两个参数
    return func(x, y)

outer(inner, 10, 20)     # 把inner函数作为参数传给outer函数

要点:函数可以传来传去

函数作用域

作用域是名字可以被搜索到的范围和规则,按照 L (本地)、E (闭合) 、G(全局)、B (内建) 四个作用域依次搜索

函数可以引用外部的作用域中的名字, 内层inner函数引用了enclosing作用域中的y

更多参考命名空间和作用域

二、闭包


前提:一定是嵌套的函数

如果一个内层函数对 enclosing (非全局非本地 )作用域中的名字进行了引用,那么这个内层函数就是个闭包 (closure)

上面的outer函数里面定义了inner函数(内层函数), 这个inner函数引用了enclosing作用域中的a,b,x,那么这个inner函数就是个闭包,引用的a,b,x也称作自由变量。

闭包函数有个__closure__属性可以得到引用的自由变量,它返回的是由cell对象组成的tuple

>>> f = outer(3)
>>> f.__closure__   # 获得引用的自由变量
(<cell at 0x7fb902b3ba98: int object at 0x8a8d40>, <cell at 0x7fb902b3b918: int object at 0x8a8e80>, <cell at 0x7fb902b3b978: int object at 0x8a8c60>)
>>> 
// 通过cell_contens属性可以获取到值 >>> f.__closure__[0].cell_contents 10 >>> f.__closure__[1].cell_contents 20 >>> f.__closure__[2].cell_contents 3

上面的例子需要理解: 闭包所引用自由变量不会随着外层函数执行完毕而销毁,闭包就好像携带了并记住这些变量名

闭包的例子

写了两个类似的功能

  • 如果是100分那么60为及格返回True,否则False
  • 如果是150分那么80为及格返回True,否则False

  ....... 等等, 更多类似要求 .....

def pass_100(score):
    """100分, 如果超过60分为及格, 返回True,否则False"""
    standard = 60
    return score > standard

def pass_150(score):
    """150分, 如果超过80分为及格, 返回True,否则False"""
    standard = 80
    return score > standard
......

这时又要添加一个类似的功能,比如200分, 120分及格为True, 否则False,于是如法炮制

def pass_200(score):
    """200分, 如果超过120分为及格, 返回True,否则False"""
    standard = 120
    return score > standard

但你发现这些功能都是类似的, 能不能做一些合并、减少重复代码呢?  那么可以使用闭包写成这样

>>> def set_pass(standard):
...     def inner(score):           
...         return score > standard # 内层inner函数引用上层作用域中的'standard'
...     return inner
... 
>>> pass_100 = set_pass(60)
>>> pass_150 = set_pass(80)
>>> pass_200 = set_pass(120)
>>> 
>>> pass_100(59)
False
>>> pass_100(61)
True
>>> pass_150(79)
False
>>> pass_150(81)
True
>>> pass_200(119)
False
>>> pass_200(121)
True
View Code

闭包小结:

  • 内层函数对enclosing作用域中的名字(自由变量)进行了引用,那么这个携带了自由变量的内层函数就是闭包
  • 装饰器就是对闭包的使用

三、装饰器


有这样的需求,需要对现有的对象比如函数、方法或类进行扩展而不修原有的代码,这是装饰器

装饰器可以动态地扩展函数、方法或类的功能而不必使用子类或修改原有代码,这就是所谓的装饰,用大白话说就是 对现有功能进行一些 "修饰 ",加点功能但又不修改原先的代码

装饰器用于切面(AOP)的场景,也是一种设计模式,较为经典的有插入日志、性能测试、事务处理等.

装饰器 = 闭包 + 高阶函数

1. 引出装饰器

很早以前写了一些功能,它是这样的

def foo():
    return 'foo'

def boo():
    return 'boo'

现在要给这个功能在添加一些功能,在函数执行的前后打印一些提示信息,你这样做的

print('开始执行foo函数')
foo()
print('foo函数执行完毕!')

print('开始执行boo函数')
boo()
print('boo函数执行完毕!')

...... 更多类似需求...

但如果还有n多函数要这样做呢? low一点复制粘贴、复制粘贴.., 这样重复的代码就更多了.

那么能不能把重复代码提前出来,并且用更优雅的方式来实现呢, 利用闭包和高阶函数

def decorator(func):   
    def inner():    # inner是个闭包,引用了enclosing作用域中的func
        print('开始执行 %s 函数 ' % func.__name__)
        result = func()
        print('%s函数执行完毕! ' % func.__name__)
        return result
    return inner

def foo():
    return 'foo'

def boo():
    return 'boo'

foo = decorator(foo)    # 把foo函数作为参数传给decorator函数并执行, 最终的结果是 foo -> inner (foo实际上指向的是inner)
boo = decorator(boo)    # 把boo函数作为参数传给decorator函数并执行, 最终的结果是 boo -> inner (boo实际上指向的是inner)
foo()                   # 实际上执行的是inner函数
boo()                   # 实际上执行的是inner函数

我们可以使用@装饰符语法,和上面的是等价的

def decorator(func):   # 这是一个装饰器
    def inner():
        print('开始执行 %s 函数' % func.__name__)
        result = func()
        print('%s 函数完毕!' % func.__name__)
        return result
    return inner

# foo被decorator装饰, 等价于"foo = decorator(foo)", 所以结果是 foo 指向 inner @decorator
def foo(): return 'foo'
# boo被decorator装饰, 等价于"boo = decorator(boo)", 所以结果是 foo 指向 inner @decorator
def boo(): return 'boo' foo() boo()
========== 结果 =========== 开始执行 foo 函数 foo 函数完毕! 'foo' 开始执行 boo 函数 boo 函数完毕! 'boo'

遇到 @decorator会把下面的 foo  boo 函数作为参数传给 decorator 函数并执行

即  " foo = decorator(foo) 、boo =decorator(boo)" , 那么返回的是inner (闭包), 最终的结果就是fooboo都执向inner,当执行foo()boo()实际上就是inner()。这时候原先的函数被重新加工了

以上就是装饰器,可以在不修改原先功能的情况下动态添加功能

2. 装饰带有参数的函数

被装饰的函数是可能有参数的,如果装饰器没有对应的参数则会抛出异常

def decorator(func):
    def inner():
        result = func()
        return result
    return inner

@decorator
def foo(content):
    return 'foo ' + content

@decorator
def boo(content):
    return 'boo ' + content

foo('abc')

=============================== 结果 ===============================
Traceback (most recent call last):
  File "E:\NOTE\python\learnpython\deco.py", line 100, in <module>
    foo('abc')
TypeError: inner() takes 0 positional arguments but 1 was given  # 提示inner没有位置参数,但却提供了一个位置参数

因为foo函数被decorator装饰,foo指向的是闭包函数inner,当执行foo的时候实际上是执行inner,而inner并没有提供参数,这样就不能为原boo函数提供参数,所以传参的时候异常.

因此闭包函数inner也应该有参数,这样才能对应起来,可以原函数有几个,闭包函数inner也写几个

def decorator(func):
    def inner(content):
        result = func(content)
        return result
    return inner

@decorator
def foo(content):
    return 'foo' + content

@decorator
def boo(content):
    return 'foo' + content

foo('abc')
boo('abc')

但更灵活的方法是在闭包函数 inner 中使用收集参数,这样就可以接收任意位置参数或关键字参数

def decorator(func):
    def inner(*args, **kwargs):
        return func(*args, **kwargs)
    return inner

@decorator
def foo(content):
    return 'foo ' + content

@decorator
def boo(content):
    return 'boo  + content

foo('abc')
boo('abc')

3、函数可以被多个装饰器装饰 (多层装饰器)

可以理解为给函数添了加多重功能、被装饰了n道,最后返回一个闭包。对于多层装饰器主要是理解它的执行流程,看个例子

def bold(func):
  " 字符串加粗装饰器 "
    def inner(*args, **kwargs):
        result = '<b>' + func(*args, **kwargs) + '</b>'
        return result
    return inner

def italic(func):
  " 字符串斜体装饰器 "
    def inner(*args, **kwargs):
        result  = '<i>' + func(*args, **kwargs) + '</i>'
        return result
    return inner

# 等价于 italic(bold(foo))
@italic             # 后被 italic 装饰,返回一个闭包函数
@bold               # 先被 bold 装饰,返回一个闭包函数
def foo():
    return 'foo'

# 等价于 bold(italic(boo))
@bold              # 后被 bold 装饰, 返回一个闭包函数
@italic            # 先被 italic 装饰,返回一个闭包函数
def boo():
    return 'boo'

boo()  # <i><b>foo</b></i>
foo()  # <b><i>boo</i></b>

所以多层装饰器的执行流程是 ‘由下往上’ 执行,先被离的最近的装饰器装饰,返回的是一个闭包函数,然后又被上层的装饰器装饰,最终返回的是另一个闭包函数,依此类推

4、类装饰器

装饰器可以是函数或类,互相可以装饰。 使用类装饰器需要在类中定义两个特殊方法 __init__和__call__

__init__方法接收一些参数, __call__方法可以让实例像函数一样可以调用

from functools import wraps

class decorate(object):
    def __init__(self, func):
        self.func = func
        wraps(func)(self)

    def __repr__(self):
        return f'{self.func}'

    def __call__(self, *args, **kwargs):
        print(f"调用{self.func.__name__}函数")
        ret = self.func(*args, **kwargs)
        return ret

@decorate      # 遇到就把helloword函数对象传给decorate这个类装饰器并实例化,此时helloword是decorate的一个实例
def helloword():
    "myfunc"
    return 'hellword'

if __name__ == '__main__':
    print(isinstance(hellword, decorate)) 
    print(helloword())  # 当helloword执行时,会调用__call__方法,因为helloword被装饰了,是decorate的实例

# 输出
True
调用hellword函数
hellword
def decorator(cls):
    def inner(*args, **kwargs):
        print('call %s' % cls.__name__)
        return cls(*args, **kwargs)
    return inner

# 把Person类作为参数传给decorator,返回inner, 即Person 指向 inner

@decorator
class Person(object):
    def __init__(self, value):
        self.value = value


Person
p = Person('me') 
p
p.value

======== 结果 ========

<function inner at 0x000000000249ABA8>
call Person
<__main__.Person object at 0x00000000024A2080>
me
函数装饰类
class Decoclass(object):

    def __init__(self, cls):
        self.cls = cls

    def __call__(self, *args, **kwargs):
        print('Call class ---> {}'.format(self.cls.__name__))
        result = self.cls(*args, **kwargs)
        return result


# 把Person作为参数传给Decoclass类并实例化, 此时Person变成了Decoclass的一个实例
# 当执行Person('参数') 实际上是执行 Person.__call__('参数')
@Decoclass     
class Person(object):

    def __init__(self, value):
        self.value = value

# Person
if __name__ == '__main__':
    print(Person)               # <__main__.Decoclass object at 0x00000000027D9208>   // Person是Decoclass的一个实例
    p = Person('abc')           # Call class ---> Person
    print(p)                    # <__main__.Person object at 0x0000000001E99F60>
    print(p.value)              # abc
类装饰类

注:只要明白装饰器的意思就行, 函数或类都可以做装饰器

5、带有参数的装饰器

装饰器是可以有参数的,写起来层次很多但功能加灵活,它会生成一个装饰器然后进行装饰,理解就是一个能生成装饰器的装饰器.

from functools import wraps

def htmltag(tag=None):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            if tag is None:
                return f(*args, **kwargs)
            return '<{0}>{1}</{2}>'.format(tag, f(*args, **kwargs), tag)
        return wrapper
    return decorator

# 第一步 执行htmltag(tag='b') 返回一个装饰器函数decorator
# 第二步 把helloword对象 作为参数传给decorator装饰器,返回一个闭包函数wrapper
# 最后 helloword 指向的是wrapper, 执行helloword()等价于htmltag(tag='b')(helloword)()
@htmltag(tag='b')
def helloword():           
    return 'helloword'

# 第一步 执行htmltag(tag='i') 返回一个装饰器函数decorator
# 第二步 把hello_word对象作为参数传给decorator装饰器,返回一个闭包函数wrapper
# 最后hello_word指向的是wrapper, 执行helloword()等价于htmltag(tag='b')(helloword)()
@htmltag(tag='i')
def hello_word():
    return 'hello_word'

# 多层装饰,先被htmltag(tag='b')装饰
# 又被htmltag(tag='i')装饰
@htmltag(tag='i')
@htmltag(tag='b')
def hello():
    return 'hello'

helloword()    # <b>helloword</b>
hello_word()   # <i>hello_word</i>
hello()        # <i><b>hello</b></i>
from functools import update_wrapper

class htmltag(object):
    """docstring for htmltag"""

    def __init__(self, tag=None):
        self.tag = tag

    def __call__(self, f):
        def inner(*args, **kwargs):
            if self.tag is None:
                return f(*args, **kwargs)
            return '<{0}>{1}<{2}>'.format(self.tag, f(*args, **kwargs), self.tag)
        return update_wrapper(inner, f)

@htmltag(tag='b') # 执行htmltag(tag='b'),得到是htmltag类的实例,实际上是个装饰器,然后将helloword函数传进去(调用__call__方法)并执行
def helloword():   
    "docstring for func"
    return 'helloword'

print(helloword())   # 输出 <b>helloword<b>
使用类装饰器
from functools import wraps


class Flask(object):
    """docstring for Flask"""

    def __init__(self):
        self.view_maps = {}

    def route(self, url):
        def decorate(func):
            self.view_maps[url] = func
            @wraps(f)
            def inner(*args, **kwargs):
                return func(*args, **kwargs)
            return inner
        return decorate

    def run(self):
        while 1:
            url = input('请输入url')
            view_func = self.view_maps.get(url, None)
            if not view_func:

            else:
                view_func()
app = Flask()

@app.route('/')
def index():
    return 'index'

app.run()
模拟Flask装饰器

6、保留被装饰的对象的元数据

装饰器有个问题是被装饰的对象元数据发送了变化,可以使用functools模块中的wraps、update_wrapper,参考这里

四、总结


理解函数相关知识

  • 函数名是函数对象的指向,可以将新的名字绑定到这个函数对象
  • 函数可以内嵌函数、返回一个函数对象
  • 高阶函数,将一个函数作为参数传给函数
  • 四个命名空间

理解闭包,内层函数引用了enclosing作用域中的名字, 像是记住携带了这些名字(自由变量),那么这个函数就是闭包

理解装饰器

  • 用于面向切面的场景,核心是给原有对象增加功能而不修改原有的代码,场景有性能测试、插入日志、拦截器、打补丁等
  • 提取一些重复的代码复用
  • 装饰器就是闭包和高阶函数的使用
  • 类横向扩展比较麻烦,而装饰器比较好解决这种问题

附: 一些装饰器例子

posted @ 2017-01-05 23:56  opss  阅读(261)  评论(0)    收藏  举报