Python装饰器

装饰器简介

在Python中,装饰器属于“元编程”的类别,“元编程”的主要目的就是创建函数或者类,并且用它们来操纵代码,比如说修改、生成或者包装已有的代码。而装饰器就是为修改或者包装函数提供了一种快捷的方式。
装饰器本质上是一个函数,而且准确地说是一个高阶函数,这个函数以其他函数为参数,并且以一个修改过的函数作为返回来替换要装饰的函数。
先看一个最简单的例子,这个装饰器什么也不做,直接返回原函数:

def identity(f):
    ''' a simple decorator'''
    return f

然后你可以这样使用装饰器:

@identity
def test(f):
    return "Hello, world"

对于上面的装饰器来说,它的实现逻辑与下面的代码相似:

def test(f):
    return "Hello, world"
test = identity(test)

可以看到装饰器的工作原理就是用新的函数替换被装饰的函数,当然上面是最简单的装饰器,它并不起什么作用。下面我们来建立一个可以对函数运行时间计时的装饰器,相对来说是比较实用的:

import time
def timethis(f):
    ''' Decorator that reports the running time.'''
    def wrapper(*args, **kwargs):
        start = time.time()
        result = f(*args, **kwargs)
        end = time.time()
        print(f.__name__, end - start)
        return result
    return wrapper

相比上一个装饰器,这个装饰器相对复杂些,在这个装饰器内部新定义了一个函数wrapper,这个函数可以接受任意个参数,并且在内部调用被装饰的函数,在调用的过程中,计算函数的运行时间并输出。这是装饰器最一般的模式。装饰器没有修改原来函数的签名,因为wrapper接受任意参数,那么传递进来的参数将原样传递给f。而且装饰器没有修改函数的返回结果。
那么,我们来看具体的装饰实例:

@timethis
def countdown(n):
    ''' 计数递减'''
    while n > 0:
        n -= 1

In [7]: countdown(100000)
countdown 0.011001110076904297
In [8]: countdown(100000000)
countdown 12.434711933135986

@timethis
def accumulation(n):
    ''' 累加'''
    sum = 0
    for i in range(n):
        sum += i
    return sum
In [10]: accumulation(10000)
accumulation 0.0009999275207519531
Out[10]: 49995000

In [11]: accumulation(10000000)
accumulation 1.2270700931549072
Out[11]: 49999995000000

装饰器更常用在控制非法访问,比如某些操作只能有管理员级别的才能执行,对于其他用户将该操作屏蔽掉,看下面一个例子:

def check_is_admin(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        fun_args = inspect.getcallargs(f, *args, **kwargs)
        if fun_args.get("username") != "admin":
            raise Exception("This user is not allowed to get food.")
        return f(*args, **kwargs)
    return wrapper

可以看到这个装饰器是用来通过检查传入函数中的用户名来控制访问,如果使用户名不是 "admin",那么调用函数时会抛出异常。相比前面的例子,这个装饰器还有两处需要注意的地方。第一,在装饰器内层定义的函数中使用了系统中functools模块中的wraps装饰器。我们要知道装饰器的原理是用新函数替换被装饰的函数,尽管装饰器不改变函数原来的参数及返回值,但是函数还是会丧失一部分原有信息,如namedoc。这些信息将被装饰函数所覆盖掉,很多情况下,我们不想丢失函数的原有信息。那么系统自带的wraps装饰器很好地解决了这个问题,@wraps(f)会将f的原有信息复制给装饰函数wrapper.第二,我们使用了系统自带的inspect.getcallargs()方法,这个方法可以方便地将传递给函数的参数转化为一个字典,那么不管username参数是位置参数还是关键字参数,装饰函数都可以很好地获得它。下面看运行实例:

@check_is_admin
def foodbar(username):
    """Do something"""
    return "food"

p, li { white-space: pre-wrap; }

In [5]: foodbar("admin")
Out[5]: 'food'

In [6]: foodbar("user")
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
Exception: This user is not allowed to get food.
In [7]: foodbar(username="admin")
Out[7]: 'food'

In [8]: foodbar.__name__
Out[8]: 'foodbar'

In [9]: foodbar.__doc__
Out[9]: 'Do something'

可以接受参数的装饰器

有时候,我们希望装饰器本身能够接受参数,这样创建的装饰器会更加灵活与实用。考虑这样的情况,你正在编写一个博客网站,用户具有下列权限:写文章,评论文章与修改文章。一个较好的办法,我们写一个通用型的装饰器,这个装饰器可以接受一个参数,这个参数是权限类别。那么我只需要定义一个装饰器,传递不同的参数给装饰器就能实现不同的装饰功能:

#对于无权限用户抛出403状态码
def permission_required(permission):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not current_user.can(permission):
                abort(403)
            return f(*args, **kwargs)
        return decorated_function

这个装饰器接受一个permission参数,并检查当前用户是否拥有该权限,如果没有将抛出403状态码。相比无参数的装饰器,这个装饰器多了一层函数,如果扒去最外层的函数,那么它将与无参数的装饰器一样。前面我们说过装饰器的原理,这里同样适用:

#对于无权限用户抛出403状态码
@permission_required("comment"):
def comment(post_id):
    ''' make a comment'''
    pass
#这与下面的定义类似
def comment(post_id):
     ''' make a comment'''
     pass
comment = permission_required("comment")(comment)

下面看传递不同的参数,装饰器实现不同功能:

#关注用户的处理视图
@main.route('/follow/<username>')
@login_required
@permission_required(Permission.FOLLOW)
def follow(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        flash("Invalid user.")
        return redirect(url_for('.index'))
    if current_user.is_following(user):
        flash('You have already following this user.')
        return redirect(url_for('.user', username=username))
    current_user.follow(user)
    flash("You are now following {0}.".format(username))
    return redirect(url_for('.user', username=username))


#管理评论视图
@main.route('/moderate')
@login_required
@permission_required(Permission.MODERATE_COMMENTS)
def moderate():
    page = request.args.get('page', 1 ,type=int)
    pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(page,\
                per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'], error_out=False)
    comments = pagination.items
    return render_template('moderate.html', comments=comments, pagination=pagination, page=page)

当然一个函数可以接受多个装饰器。
$ f(x)=\left{
\begin{aligned}
x & = & \cos(t) \
y & = & \sin(t) \
z & = & \frac xy
\end{aligned}
\right.
$
未完待续
学习参考

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

推荐阅读更多精彩内容

  • 每个人都有的内裤主要功能是用来遮羞,但是到了冬天它没法为我们防风御寒,咋办?我们想到的一个办法就是把内裤改造一下,...
    chen_000阅读 1,366评论 0 3
  • 虽然人们能利用函数闭包(function clouser)写出简单的装饰器,但其可用范围常受限制。多数实现装饰器的...
    gomibako阅读 1,026评论 0 4
  • 一.函数装饰器 1.从Python内层函数说起 首先我们来探讨一下这篇文章所讲的内容Inner Functions...
    软体动物Ai阅读 3,260评论 0 14
  • 装饰器的主要作用:代码复用、装饰函数! 简单装饰器 现在有三个函数 每个函数都有自己的功能! 这时我想让这些函数在...
    学编程的Dreamer阅读 697评论 0 8
  • 关注护肤有好长一段时间了,最近打算整理一下自己觉得好用/不好用的护肤品和化妆品,给大家一个参考。本人肤质混合偏干,...
    林中之象阅读 921评论 0 4