Python 闭包与装饰器

  • 原创
  • Madman
  • /
  • 2018-06-05 15:02
  • /
  • 0
  • 327 次阅读

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

1. 闭包

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

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

In [1]: def line(a, b):
   ...:     def get_y_axis(x):
   ...:         return a * x + b  # 内部函数使用了外部函数的变量a和b
   ...:     return get_y_axis     # 返回值是闭包函数名,注意不是函数调用没有小括号
   ...: 
   ...: 

In [2]: L1 = line(1, 1)  # 创建一条直线: y=x+1

In [3]: L1
Out[3]: <function __main__.line.<locals>.get_y_axis(x)>

In [4]: L1(3)  # 获取第一条直线中,横坐标是3时,纵坐标的值
Out[4]: 4

In [5]: L2 = line(2, 3)  # 创建一条直线: y=2x+3

In [6]: L2
Out[6]: <function __main__.line.<locals>.get_y_axis(x)>

In [7]: L2(3)  # 获取第二条直线中,横坐标是3时,纵坐标的值
Out[7]: 9

In [8]: L3 = line(2, 3)  # 再创建一条直线: y=2x+3

In [9]: L3
Out[9]: <function __main__.line.<locals>.get_y_axis(x)>

In [10]: L2 == L3  # 每次调用line()返回的都是不同的函数,即使传入相同的参数
Out[10]: False

In [11]: L2 is L3
Out[11]: False

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

# 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

2. 装饰器

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

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

举个实例,假设你写好了100个Flask中的路由函数,现在要在访问这些路由函数前,先判断用户是否有权限,你不可能去这100个路由函数中都添加一遍权限判断的代码(如果权限判断代码为5行,你得加500行)。那么,你可能想把权限认证的代码抽离为一个函数,然后去那100个路由函数中调用这个权限认证函数,这样只要加100行。但是,根据开放封闭原则,对于已经实现的功能代码建议不能修改, 但可以扩展,因为可能你在这些路由函数中直接添加代码会导致原函数出现问题,那么最佳实践是使用装饰器

2.1 被装饰的函数无参数

如果原来是调用f1()、f2() ... ,我们只要让用户还是调用f1()、f2() ... ,即他们调用的函数名还是保持不变,但实际执行的函数体代码已经变了(Python中函数也是对象,函数名只是变量,可能改变它引用的函数体对象):

没有使用装饰器之前:

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.2 被装饰的函数有参数

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

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

f1(10)

如果被装饰的函数有参数,调用f1(10)此时实际调用的是inner(10),而装饰器中的闭包inner没有定义参数,所以会报错:

Traceback (most recent call last):
  File "test.py", line 14, in <module>
    f1(10)
TypeError: inner() takes 0 positional arguments but 1 was given

那么如果我给inner定义一个形参呢?

def login_required(func):
    def inner(a):  # inner定义了一个形参,名字随意
        if func.__name__ == 'f1':
            print(func.__name__, ' 权限验证成功')
            func()
        else:
            print(func.__name__, ' 权限验证失败')
    return inner

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

f1(10)

还是会报错,原因是执行inner函数体时,当执行到func()时,它指向传入装饰器的原函数f1的引用,而原f1需要一个位置参数:

f1  权限验证成功
Traceback (most recent call last):
  File "test.py", line 14, in <module>
    f1(10)
  File "test.py", line 5, in inner
    func()
TypeError: f1() missing 1 required positional argument: 'a'

所以正确的做法是:

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(a)函数,但是假如有一个f2(a, b, c)有三个参数呢,肯定报错,所以还要修改装饰器,使用Python中的*args**kwargs来匹配任意长度的位置参数或关键字参数:

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

2.3 被装饰的函数有返回值

如果使用2.2的装饰器,修改f1()函数定义,它里面有return返回值,将会有问题:

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)

# 输出结果中返回的是None,而不是hello, world
f1  权限验证成功
function f1, args: a=10, b=20, c=30
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

2.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...

2.5 使用@wraps

def logging(level='INFO'):
    def decorator(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
错误的函数签名: wrapper
错误的函数文档: print log before a function.

原因是调用f1(10, 20, 30),实际是调用装饰器中的wrapper(),所以打印出来的函数签名和文档都是wrapper的,可以使用functools模块的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

2.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>

注意输出结果中,多个装饰器的顺序,包装时,是从下往上的,先包装一个小箱子@makeItalic,会进入makeItalic装饰器中函数体执行(注意它里面的func指向test函数体),print('这是斜体装饰器')会输出,然后返回闭包italic_wrapped。再包装一个大箱子@makeBold,会进入makeBold装饰器中函数体执行(注意它里面的func指向italic_wrapped),print('这是粗体装饰器')会输出,然后返回闭包blod_wrapped

而调用res = test()时,先执行右边的test(),相当于拆包装,肯定是从最外层开始拆。此时的test指向的是blod_wrapped,所以执行test()会执行blod_wrapped(),即print('---1---')会输出。然后执行到return '<b>' + func() + '</b>',由于这里的func指向italic_wrapped,所以先去执行italic_wrapped(),即print('---2---')会输出。再执行到return '<i>' + func() + '</i>',同样这里的func指向原test函数体,所以先去执行它,即print('---3---')会输出,并且返回'Hello, world'。这时,回到italic_wrapped函数体返回'<i>Hello, world</i>',然后再回到blod_wrapped函数体返回'<b><i>Hello, world</i></b>',即调用test()的最终返回值,并赋值给res变量,然后打印输出到控制台。

建议使用pythontutor在线调试,可以清楚的看到装饰器内部是如何执行的。

2.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

3. 装饰器实例

更多实例请参考:http://python3-cookbook.readthedocs.io/zh_CN/latest/chapters/p09_meta_programming.html

3.1 函数执行时间统计

import time
from functools import wraps

def timethis(func):
    '''
    Decorator that reports the execution time.
    '''
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end-start)
        return result
    return wrapper

@timethis
def countdown(n):
    '''Counts down'''
    while n > 0:
        n -= 1
    return 'done'

res = countdown(100000)
print(res)

# 输出结果:
countdown 0.0120086669921875
done
@timethis
def countdown(n):
    pass

等价于:

def countdown(n):
    pass
countdown = timethis(countdown)

内置的装饰器比如@staticmethod@classmethod@property原理也是一样的,下面两个代码片段是等价的:

class A:
    @classmethod
    def method(cls):
        pass

class B:
    # Equivalent definition of a class method
    def method(cls):
        pass
    method = classmethod(method)

3.2 插入日志

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!
分类: Python进阶
标签: Closure Decorator
未经允许不得转载: LIFE & SHARE - 王颜公子 » Python 闭包与装饰器

分享

作者

作者头像

Madman

如果博文内容有误或其它任何问题,欢迎留言评论,我会尽快回复; 或者通过QQ、微信等联系我

0 条评论

暂时还没有评论.

发表评论前请先登录