一周一个Python语法糖:(一)装饰器

Decorator

首先,我们来认识一下装饰器是什么:
装饰器是给现有的模块增添新的小功能
(在不改变原有模块功能的基础上)

假如我有个简单笔,它只能用一种颜色进行写字
我现在给它加上一只笔芯,它能换种颜色写字(又能换回来~)
这就是装饰器的朴素比喻

一、初探装饰器

手动写个装饰器吧


#自定义装饰函数
def decorator(fn):
    def wrapper(*args):
        #这里装饰器的作用是在函数调用前增加一句话表示装饰成功

        print("this is decorator fo %s" % fn.__name__)
        fn(*args)
    return wrapper
def hello(name):
    print('hello,%s' %name)
if __name__=="__main__":
    #用赋值的形式进行装饰器
    hello=decorator(hello)
    hello("cool")

输出结果为:

Paste_Image.png

首先,我们要知道,在python中,函数也是一种对象(万物皆对象!)

  1. 函数可以赋值给一个变量(学过C语言的可以联想下函数指针)

  2. 函数可以定义在另一个函数内部

这也意味着一个函数可以返回另一个函数

hello=decorator(hello)

这一句代码中,将hello函数作为变量传入decorator装饰器中,然后hello方法在decorator中的函数wrapper函数实现,同时包装新的功能,将新的函数wrapper作为变量返回 ,所以hello的新值是 经过decorator装饰的wrapper新方法。

所以,装饰器装饰函数的时候,将函数作为变量传入装饰器内部,实际调用的是装饰器内部的函数(添加新功能之后的函数)

二、 @语法糖

Python 中装饰器语法并不用每次都用赋值语句。
在函数定义的时候就加上@+装饰器名字即可
再来我们刚才的例子吧:

def decorator(fn):
    def wrapper(*args):
        print("this is decorator fo %s" % fn.__name__)
        fn(*args)
    return wrapper
@decorator
def hello(name):
    print('hello,%s' %name)
if __name__=="__main__":
    hello("cool")

2.装饰器的顺序:

比如我们有两个装饰器:

@decorator_one
@decorator_two
def hello()
    pass

这句代码实际上类似于:

hello=decorator_one(decorator_two(hello))

两个装饰器一层层地往外装饰

3.带参数的装饰器

我们说过,装饰器其实也是一种函数,所以它自身也是能带参数的

@decorator(arg1, arg2)
def func():
    pass

类似于

func = decorator(arg1,arg2)(func)

来个实际点的例子吧:
我们手写html的时候需要各种补全(那个用编辑器的当然爽得飞起!)
但是,如果是在python中用字符串去表示html标签的时候,就~坑爹了
总不能每个标签我都写一个方法吧
最方便的方法,写一个带参数的装饰器!
HTML.py

def makeHtmlTag(tag, *args, **kwargs):
    def real_decorator(fn):
        css_class = " class='{0}'".format(kwargs["css_class"]) \
                                     if "css_class" in kwargs else ""
        def wrapped(*args, **kwargs):
            return "<"+tag+css_class+">" + fn(*args, **kwargs) + "</"+tag+">"
        return wrapped
    return real_decorator

@makeHtmlTag(tag="b", css_class="bold_css")
@makeHtmlTag(tag="i", css_class="italic_css")
def hello():
    return "hello world"

print(hello())
 
print(hello())
 

运行结果:

Paste_Image.png

关于几点说明:
(1)在装饰器makeHtmlTag中,*args代表了参数元组,假如你传入的的参数分别是1,2,3,4,则args=(1,2,3,4)
**kwds则参数字典,返回一个key为参数变量名,value为变量值的字典
这样我们就可以很方便地不用改动函数本身去获取函数传递的参数并进行装饰了
(2)使用装饰器的时候有个缺陷就是不能在过程中更改某个装饰器的参数值(比如该例子中 hello 的便签就永远是b,i了)

如果你觉得这样写太!麻!烦!了!什!么!鬼!
为什么我要在函数体中再定义一个函数体!!!!
难道还要我一层层剥开你的心吗?

4. 用类的方式去写一个装饰器

class makeHtmlTagClass(object):
 
    def __init__(self, tag, css_class=""):
        self._tag = tag
        self._css_class = " class='{0}'".format(css_class) \
                                       if css_class !="" else ""
 
    def __call__(self, fn):
        def wrapped(*args, **kwargs):
            return "<" + self._tag + self._css_class+">"  \
                       + fn(*args, **kwargs) + "</" + self._tag + ">"
        return wrapped
 
@makeHtmlTagClass(tag="b", css_class="bold_css")
@makeHtmlTagClass(tag="i", css_class="italic_css")
def hello(name):
    return "Hello, {}".format(name)
 
print hello("Hao Chen")

关于说明:
(1)我们将整个类作为一个装饰器,工作流程:
通过__init__()方法初始化类
通过__call__()方法调用真正的装饰方法
(2)当装饰器有参数的时候,init() 成员就不能传入fn了,而fn是在call的时候传入的。(fn代表要装饰的函数)

decorator万能的?

No!No!No!
有时候我想加入日志系统。来记录我某个函数的运行情况。

import time
def logger(fn):
    def wrapper(*args,**kwargs):
        ts=time.time()
        print('start run the function %s' % fn.__name__)
        result=fn(*args,**kwargs)
        te=time.time()
        print('run end! it continue %.2f '%(te-ts))
        return result
    return wrapper

@logger
def hello():
    print('start running')
    time.sleep(2)
    return 2

hello()
print(hello.__name__)

运行结果:

Paste_Image.png

W T F?

我的hello.__name__不是应该是hello吗?
唯一解释:就想一开始说的,装饰器原理:

hello=decorator(hello)

hello实际上已经变成了经过装饰器修饰的方法了
(主公,我身在曹营心在汉呀!!!!)
怎么办?
Python的functool包中提供了一个叫wrap的decorator来消除这样的副作用
新版logger.py


from functools import wraps
import time
def logger(fn):
    @wraps(fn)
    def wrapper(*args,**kwargs):
        ts=time.time()
        print('start run the function %s' % fn.__name__)
        result=fn(*args,**kwargs)
        te=time.time()
        print('run end! it continue %.2f '%(te-ts))
        return result
    return wrapper

@logger
def hello():
    print('start running')
    time.sleep(2)
    return 2

hello()
print(hello.__name__)

运行结果:

Paste_Image.png

5.装饰器获取参数的值
比如我某个函数是批量运行的,我需要加个日志系统来知道这个函数进行了多少次,,这就需要获取函数运行时的参数了
方法:用inspect模块的getcallargs方法去获取原函数的参数
返回的是一个字典,根据字典的key去获取参数的值

from functools import wraps
from inspect import getcallargs
class Logger(object):
    def __init__(self,filename=''):
        if filename!='':
            self._filename=filename
        else:
            self._filename="log/errorlog"
    def __call__(self,fn):
        @wraps(fn)
        def wrapper(*args,**kwargs):
            func_args=getcallargs(fn,*args,**kwargs)
            if 'param1' in func_args.keys():
                param1=func_args['param1']
            if 'param2' in func_args.keys():
                param2=func_args['param2']
            with open(self._filename,'a') as logfile_handle:
                logfile_handle.write(param1+'/'+param2+'\t finished\n')
                logfile_handle.flush()
            result=fn(*args,**kwargs)
            return result
        return wrapper

@Logger(filename='testlog')
def test(param1,param2,param3,param4,param5):
    print('Ok,finished')
    return param5

if __name__=="__main__":
    print(test('1','2','3','4','5'))
    print(test('s','g','r','e','b'))

运行结果:

2017-04-03 19-29-07屏幕截图.png

打开日志文件:

2017-04-03 19-29-56屏幕截图.png

我们拿到了函数的第一个参数跟第二个参数的值并保存到文件中

一些装饰器的例子(日志那个请看上文)

from functools import wraps
def memo(fn):
    cache = {}
    miss = object()
 
    @wraps(fn)
    def wrapper(*args):
        result = cache.get(args, miss)
        if result is miss:
            result = fn(*args)
            cache[args] = result
        return result
 
    return wrapper
 
@memo
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

这是斐波拉契数例的递归算法。
因为fib(1) fib(0)是重复计算的值,将其利用装饰器预先缓存(先计算好fib(1),fib(0)),调用的时候直接返回字典的值,达到优化fib(n)的思路
很经典
当我读懂这段代码的时候,
我内心大喊一声:WO CAO! 还能这样玩!

2.web后端通过URL的路由来调用相关注册

class MyApp():
    def __init__(self):
        self.func_map = {}
 
    def register(self, name):
        def func_wrapper(func):
            self.func_map[name] = func
            return func
        return func_wrapper
 
    def call_method(self, name=None):
        func = self.func_map.get(name, None)
        if func is None:
            raise Exception("No function registered against - " + str(name))
        return func()
 
app = MyApp()
 
@app.register('/')
def main_page_func():
    return "This is the main page."
 
@app.register('/next_page')
def next_page_func():
    return "This is the next page."
 
print app.call_method('/')
print app.call_method('/next_page')

注:decorator类中没有call(),但是wrapper返回了原函数。所以,原函数没有发生任何变化。
这例子只是用来注册url 方法并调用
防止web访问位置url~

无返回值的异步多线程调用(涉及数据变化请用锁)

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

推荐阅读更多精彩内容