Python装饰器

装饰器作为Python当中非常经典和实用的feature,在项目当中应用是非常广泛的,比如说记录运行时间,缓存,鉴权管理,等等都会使用到装饰器。我在学习装饰器的过程中,其实并不算非常的顺利,其中也遇到了不少的坑,不过当学完装饰器之后,使用起来,那别提有多爽了,在本文当中我将讲述一下我学习装饰器的历程,希望对大家有所启发,如有理解的不对或者不到位的地方,也欢迎各位读者斧正。

基础知识

函数作为Python当中的一等公民(first-class citizen),函数也是对象,可以吧函数复制与变量,代码如下:

def func(msg):
    print(f"Got a message {msg}")

seed_message = func
seed_message("Hello World!")

这里,我们吧函数func赋值给了变量send_message,这样调用send_message就相当于调用了func

函数同样可以作为参数传到另一个函数当中。

def get_message(msg):
    return f'Got a messsage: {msg}'

def deal_msg(deal_strategy, msg):
    print(deal_strategy(msg))

deal_msg(get_message, 'Hello World!')

在这个例子当中,我们将函数get_message作为参数,传递到了deal_message当中,然后调用它。

可以在函数中定义函数,也就是函数的嵌套,代码如下:

def func(msg):
    def get_message(msg):
        print(f"Got a message: {msg}")
    return get_message(msg)

func("Hello World!")

这里,在函数func内部定义了一个函数get_message,然后调用后作为func的返回值返回。

函数的返回值也可以是函数对象(闭包),比如下面的例子:

def closure():
    def get_message(msg):
        print(f"Got a message: {msg}")
    return get_message

send_message = closure()
send_message("Hello World!")

这里,函数closure的返回值是一个函数对象,之后将这个函数的返回值进行调用,得到了正确的输出。

简单的装饰器

有了上面的基础知识,我们可以来看今天的主角装饰器了,首先来看一个简单例子。

def decorator(func):
    def wrapper():
        print("wrapper of decorator")
        func()
    return wrapper

def retouched_func():
    print("This is retouched function.")

retouched_func = decorator(retouched_func)

retouched_func()

这里,我们通过函数作为参数传递给decorator函数,然后首先调用wrapper打印第一句,然后调用func也就是实际传递进来的参数retouched_func打印第二句,这里,我们改变了retouched_func的行为,但是retouched_func函数本身并没有发生变化,也就是说,这个方法是无入侵的。

实际上,这段代码虽然实现了装饰器的功能,但是,这个看起来太不pythonic了,在Python当中有更加简单和优雅的表示:

def decorator(func):
    def wrapper():
        print("wrapper of decorator")
        func()
    return wrapper

@decorator
def retouched_func():
    print("This is retouched function.")

retouched_func()

这里的@实际上是一个语法糖,相当于之前的retouched_func = decorator(retouched_func)写法,因为是语法糖,因此这个写法更加的简洁,因此,如果需要把一个函数做类似的装饰,只需要使用@就可以了。

带参数的装饰器

这里,就会有一个问题,如果说有参数需要传递给装饰需要怎么办呢? 一个非常简单的做法就是可以在对应的装饰器函数wrapper上添加对应的参数,例子如下:

def decorator(func):
    def wrapper(msg):
        print("wrapper of decorator")
        func(msg)
    return wrapper

@decorator
def retouched_func(msg):
    print(f"This is retouched function. params is {msg}")

retouched_func("Hello World!")

那么,问题又来了,如果说,有另外一个函数也想用这个装饰器,但是这个新的函数有两个餐宿,又该怎么办呢?

通常情况下会使用*args**kwargs,作为装饰器内部函数wrapper的参数,其中*args**kwargs,表示接受任意数量和类型的参数,因此装饰器就可以写成下面的形式:

def decorator(func):
    def wrapper(*args, **kwargs):
        print('wrapper of decorator')
        func(*args, **kwargs)
    return wrapper

带有自定义参数的装饰器

装饰器不仅可以接受原函数任意类型和数量的参数,除此之外,它还可以接受自定义的参数,举个例子,我们想写一个装饰器重复执行同一个函数,然后在装饰器可以自定义执行的次数。

def repeat(num):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(num):
                print(f"wrapper of decorator, func run {i} times")
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(4)
def retouched_func():
    print("Hello World!")

retouched_func()

修饰之后函数的变化

下面,我们来看一个有趣的现象,来看代码:

def decorator(func):
    def wrapper(msg):
        print("wrapper of decorator")
        func(msg)
    return wrapper

@decorator
def retouched_func(msg):
    print(f"This is retouched function. params is {msg}")

print(retouched_func.__name__)
help(retouched_func)

这里,可以发现,retouched_func被修饰之后,他的元信息变了,这里的函数已经被wrapper取代了,为了解决这个问题,我们通常使用内置的一个装饰器@functools.wrap,它会帮助保留原函数的元信息(也就是将原函数的元信息,拷贝到对应的装饰器函数里)。

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("wrapper of decorator")
        func(*args, **kwargs)
    return wrapper

@decorator
def retouched_func(msg):
    print(f"This is retouched function. params is {msg}")

print(retouched_func.__name__)

类装饰器

前文主要介绍了函数作为装饰器的做法,实际上,类也可以作为装饰器,类装饰器主要依赖于函数__call__(),每当你调用一个类的示例时,函数__call__()就会被执行一次,直接来看代码吧:

class Decorator:
    def __init__(self, func):
        self.func = func
    
    def __call__(self, *args, **kwargs):
        print('This is in class decorator!')
        return self.func(*args, **kwargs)

@Decorator
def example():
    print("Hello World!")

example()

装饰器的嵌套

在Python当中,是支持多个装饰器的,调用顺序从上到下,来看一个例子就明白了:

import functools

def decorator1(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('execute decorator1')
        func(*args, **kwargs)
    return wrapper


def decorator2(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('execute decorator2')
        func(*args, **kwargs)
    return wrapper


@decorator1
@decorator2
def example(message):
    print(message)


example('Hello World!')

装饰器的用法示例

到这里,装饰器的基本概念和语法,我就描述完了,下面来看几个实际使用装饰器的例子。

身份认证

身份认证很容易理解,比如登录APP的时候需要输入用户名和密码,然后服务端就会确认你的身份,如果认证通过就可以进行某些操作,否则便不可以,下面我们来看一个简单的代码示例。


import functools

def authenticate(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        request = args[0]
        if check_user_logged_in(request):
            return func(*args, **kwargs)
        else:
            raise Exception('Authentication failed')
    return wrapper
    
@authenticate
def post_comment(request, *args, **kwargs)
    pass

这里,装饰器会首先检查是否登录, 如果已经登录,便可以执行post_commet的操作,否则便无法执行这个操作。

日志记录

日志记录是一个非常常见的案例,比如在实际工作中,我需要测试某个函数的执行时间,但是我又不想入侵这个函数添加计时的操作,那么装饰器就会极大的发挥它的无入侵性的这个特点:


import time
import functools

def log_execution_time(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        res = func(*args, **kwargs)
        end = time.perf_counter()
        print('{} took {} ms'.format(func.__name__, (end - start) * 1000))
        return res
    return wrapper
    
@log_execution_time
def calculate_similarity(items):
    pass

这里,装饰器log_execution_time记录某个函数的运行时间,并返回其执行结果。如果你想计算任何函数的执行时间,在这个函数上方加上@log_execution_time即可。

缓存

缓存装饰器也非常的常见,Python就内置了一个LRU cache的装饰器,具体的细节请自行查阅相关资料,这里我就不过多解释了。

总结

本文介绍了Python当中装饰器的用法,

Decorators is to modify the behavior of the function through a wrapper so we don’t have to actually modify the function.

然后介绍了几个装饰器典型的应用,合理使用装饰器,往往可以极大的提高程序的可读性和运行效率。

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

推荐阅读更多精彩内容