Python闭包和装饰器

闭包

闭包是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。注意是”引用“,不能在闭包函数内修改这个变量。

例如:

def print_msg():
    msg = "I'm closure"

    # printer是嵌套函数
    def printer():
        print(msg)

    return printer


closure = print_msg()  # 这里获得的就是一个闭包
closure()  # I'm closure

msg是一个局部变量,在print_msg函数执行之后应该就不会存在了。但是嵌套函数引用了这个变量,将这个局部变量封闭在了嵌套函数中,这样就形成了一个闭包。

闭包就是引用了自由变量的函数,这个函数保存了执行的上下文,可以脱离原本的作用域独立存在。

问:那么闭包存在有什么意义呢?什么时候会用到闭包?

Python中的装饰器Decorator,假如你需要写一个带参数的装饰器,那么一般都会生成闭包。下面装饰器中的wrapper就是一个闭包函数
我个人认为,闭包存在的意义就是它夹带了外部变量(私货),如果它不夹带私货,它和普通的函数就没有任何区别。同一个的函数夹带了不同的私货,就实现了不同的功能。闭包和面向接口编程的概念很像,也可以把闭包理解成轻量级的接口封装。

装饰器的写法

装饰器本质是一个函数,返回值也是一个函数对象。它可以为已存在的函数或对象在不修改任何代码的前提下添加额外的功能。

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

经常写一个装饰器,做权限校验。如下:

from functools import wraps

def login_required(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
    	# 做一些登录检查
    	return fun(*args, **kwargs)
    
    return wrapper


@user_blueprint.route('/user/update', methods=['PATCH'])
@login_required
def update_user_info(payload):
    # 更新用户信息
    return {...}, 201

# 相当于
# update_user_info = login_required(update_user_info)

相当于把update_user_info作为参数传递给了login_required,在update_user_info执行之前做一些检查操作。

@warp(func)它能把原函数的元信息拷贝到装饰器里面的func函数中。函数的元信息包括文档字符串name参数列表等等。

一步步分析上面的装饰器

首先最简单的应该这样写,不带参数:

def login_required(func):
    def wrapper():
    	# 做一些登录检查
    	return fun()
    return wrapper


@user_blueprint.route('/user/update', methods=['PATCH'])
@login_required
def update_user_info():
    # 更新用户信息
    return {...}, 201

此时,update_user_info函数没有传递参数,但是一般的更新操作是有参数传递的,那么我们把参数传递给wrapper函数就可以了:

def login_required(func):
    def wrapper(payload):
    	# 做一些登录检查
    	return fun(payload)
    
    return wrapper


@user_blueprint.route('/user/update', methods=['PATCH'])
@login_required
def update_user_info(payload):
    # 更新用户信息
    return {...}, 201

此时,装饰器可以接受带参数的函数了。但装饰器不止提供给update_user_info用,可能有许多的其它函数,它们有各种各样的参数,单用payload就不够了,还好Python提供了可变参数*args**kwargs,最终装饰器改为:

def login_required(func):
    def wrapper(*args, **kwargs):
    	# 做一些登录检查
    	return fun(*args, **kwargs)
    
    return wrapper


@user_blueprint.route('/user/update', methods=['PATCH'])
@login_required
def update_user_info(payload):
    # 更新用户信息
    return {...}, 201

这样就满足了各种类型的参数格式。到这里为止,还存在另一个很小的问题:错误的函数签名和文档(元信息)

函数执行完后,我们打印一下:

print(update_user_info.__name__)  # wrapper

update_user_info.__name__怎么变成闭包函数wrapper的函数名了?这是因为装饰器的调用方式是update_user_info = login_required(update_user_info)login_required返回的就是闭包函数wrapper,所以update_user_info的元信息就全部变成了wrapper的元信息了。

使用标准库里的functools.wraps,可以基本解决这个问题。

from functools import wraps

def login_required(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
    	# 做一些登录检查
    	return fun(*args, **kwargs)
    
    return wrapper


@user_blueprint.route('/user/update', methods=['PATCH'])
@login_required
def update_user_info(payload):
    # 更新用户信息
    return {...}, 201

@wraps装饰器把update_user_info的元信息传递了wrapper。这时候再调用update_user_info.__name__,就不会出现上述错误的元信息了!

到此结束,这个装饰器函数分析完成,和本文开头的代码一模一样。

Tips: @wraps传递了大部分的元信息,但函数签名和源码还是拿不到,要彻底解决这个问题可以借用第三方包,比如wrapt

装饰器进阶

带参数的装饰器和类装饰器属于进阶的内容。

带参数的装饰器

其实很简单。在应用装饰器记录日志的场景中,希望进入某个函数后打出Log信息,而且还需指定log的级别,那么应该这样写:

from functools import wraps


def logger(level):
    def wrapper(func):
        @wraps(func)
        def inner_wrapper(*args, **kwargs):
            print("[{level}]: enter function {func}()".format(level=level, func=func.__name__))
            # log.add("[{level}]: enter function {func}()".format(level=level, func=func.__name__))
            func(*args, **kwargs)
        return inner_wrapper
    return wrapper


@logger(level='INFO')
def insert(payload):
    pass


@logger(level='DEBUG')
def do(payload):
    pass

# 相当于
# insert = logging(level='INFO')(insert)
# do = logging(level='DEBUG')(do)

可以这么理解,当@logging(level='INFO')被使用时,它其实是一个函数,会马上被执行,然后它返回了一个装饰器wrapperinsert函数是传递给了wrapperwrapper内部和原来一毛一样!

用类实现装饰器

装饰器定义是接收一个可调用对象返回一个可调用对象,python中的可调用对象一般是函数。但如果重载了某个对象的内置方法__call__,那这个对象就是可调用的了,如果这个对象再返回一个可调用函数,那它就可以达到装饰器的效果。

class logger:
    def __init__(self, func):  # 接受一个函数
        self.func = func

    def __call__(self, *args, **kwargs):
        print("enter function {func}()".format(func=self.func.__name__))
        return self.func(*args, **kwargs)  # 返回一个函数

@logger
def insert(payload):
    pass

此时logging就是一个装饰器。

让这个类装饰器可以带参数,还例如Log日志级别:在构造函数里接受的就不是一个函数,而是传入装饰器参数。然后在重载__call__方法是就需要接受一个函数并返回一个函数。

class logger:
    def __init__(self, level):  # 装饰器参数
        self.level = level

    def __call__(self, func):  # 接收一个函数
        def wrapper(*args, **kwargs):
            print("[{level}]: enter function {func}()".format(level=self.level, func=func.__name__))
            func(*args, **kwargs)
        return wrapper  # 返回一个函数


@logger(level='INFO')
def insert(payload):
    pass

类内置的装饰器

装饰器不能装饰类内置的装饰器@property, @classmethod, @staticmethod,因为这三个返回的不是可调用对象。

@property

把一个类函数变成一个属性,由类对象使用。obj.get_fllower_list, get_fllower_list是一个返回粉丝列表函数,@property装饰后get_fllower_list成为类对象的一个属性。

class Test:
    def get_fllower_list(self):
        return self.__fllower_list__

# t = Test()
# flower_list = t.get_fllower_list()

@classmethod

类方法,当我们需要和类直接交互,类方法是最好的选择。类方法与实例方法类似,但是传递的不是类的实例,而是类本身。可用类名直接调用,也可用类对象调用,第一个参数是cls,不是self。

class Test:
    def func(self):
        pass

Test.func()
t = Test()
t.func()

@staticmethod

静态方法,和类相关但是又不需要类和实例中的任何信息、属性等等。如果把这些方法写到类外面,这样就把和类相关的代码分散到类外,使得之后对于代码的理解和维护都是巨大的障碍。而静态方法就是用来解决这一类问题的。

比如我们检查是否开启了日志功能,这个和类相关,但是跟类的属性和实例都没有关系。

就是一个普通函数,只是写在了类中,无self,cls,可用类名调用,也可以想调普通方法一样使用(self.func())

class Test:
    
    @staticmethod
    def func():
        # 与类功能无关的代码
        if log_enabled:
            print("log is enabled")
        else:
            print("log is disabled")

# Test.func()

小结

如何优化装饰器?

  1. 嵌套的装饰器看起来不太直观,用第三方包提高装饰器的可读性:decorator.py
  2. 被装饰函数元信息部分丢失问题,如果要传递全部元信息,可以用第三方包wrapt解决。
posted @ 2020-04-26 15:37  961897  阅读(246)  评论(0编辑  收藏  举报