Python3 装饰器

1. 装饰器作用

装饰模式有很多经典的使用场景,例如插入日志、性能测试、事务处理等等,有了装饰器,就可以提取大量函数中与本身功能无关的类似代码,从而达到代码重用的目的,说白了所谓装饰器,就是在不影响函数本身的情况下,为函数增加其他的附带功能,用于方便记录信息以及其他

2. 一个实例

某程序员接到要优化之前做的项目,首先他需要计算出各个模块的运行时间。现在有两种方案,一个是把程序执行时间功能写到模块内部,另一个则是另外写一个函数,但是修改模块内部是程序员大忌,所以采取第二种方案

#!/usr/bin/env python3
#-*-coding:UTF-8-*-

import time


def program_times(userMain):
    startTime = time.time()
    userMain()
    endTime = time.time()
    msecsTime = (endTime - startTime) * 1000
    print("程序运行时间:%s 毫秒" % msecsTime)

def main():
    print("程序运行开始...")
    time.sleep(0.5)
    print("程序运行结束...")

program_times(main)

运行结果:

Geek-Mac:Downloads zhangyi$ python3 Decorator.py 
程序运行开始...
程序运行结束...
程序运行时间:500.9748935699463 毫秒
Geek-Mac:Downloads zhangyi$ 
补充:
  • 程序最后一行是将 main 函数作为参数传递给 program_times 函数

  • 因为函数也是一个对象(或者叫做是一个指针,就是内存地址,C 程序员应该能理解),对象可以被赋值给变量,所以,通过变量也能调用该函数,也直接传递给其他函数(将 main 函数地址传给 program_times)

  • 函数对象有一个 __name__ 属性,可以拿到函数的名字,其实就是将 main 的内存地址传给 f,f 就是 main() 函数,只不过是指向内存地址相同,名字不同罢了

    >>> def main():
    ...     print("Hello World!")
    ... 
    >>> f = main
    >>> f
    <function main at 0x100756f28>
    >>> f()
    Hello World!
    >>> main()
    Hello World!
    >>> f.__name__
    'main'
    >>> main.__name__
    'main'
3. 优化实例

上面的做法有一个问题,就是所有的 main 调用处都要改为 deco(myfunc) ,下面做一些改动来避免计时功能对 main 函数调用代码的影响

#!/usr/bin/env python3
#-*-coding:UTF-8-*-

import time


def deco(userMain):
    def program_times():
        startTime = time.time()
        userMain()
        endTime = time.time()
        msecsTime = (endTime - startTime) * 1000
        print("程序运行时间:%s 毫秒" % msecsTime)
    return program_times

def main():
    print("程序运行开始...")
    time.sleep(0.5)
    print("程序运行结束...")

print("此时 main 函数的名称是:%s " % main.__name__)
main = deco(main)
print("此时 main 函数的名称是:%s " % main.__name__)

main()

运行结果:

Geek-Mac:Downloads zhangyi$ python3 Decorator.py 
此时 main 函数的名称是:main 
此时 main 函数的名称是:program_times 
程序运行开始...
程序运行结束...
程序运行时间:503.86500358581543 毫秒
Geek-Mac:Downloads zhangyi$ 
补充:
  • 语句 main = deco(main) 是将 main 作为参数传递给 deco 函数,deco 函数会将 program_times 函数返回,然后在将 program_times 函数赋值给 main。此时的 main 函数其实已经是 program_times 函数了
4. 装饰器(不带参数)

上面的实例其实已经是装饰器了,但是在 python 中,我们可以使用 @ 语法糖来精简装饰器代码

#!/usr/bin/env python3
#-*-coding:UTF-8-*-

import time


def deco(userMain):
    def program_times():
        startTime = time.time()
        userMain()
        endTime = time.time()
        msecsTime = (endTime - startTime) * 1000
        print("程序运行时间:%s 毫秒" % msecsTime)
    return program_times

@deco
def main():
    print("程序运行开始...")
    time.sleep(0.5)
    print("程序运行结束...")

print("此时 main 函数的名称是:%s " % main.__name__)

main()

运行结果:

Geek-Mac:Downloads zhangyi$ python3 Decorator.py 
此时 main 函数的名称是:program_times 
程序运行开始...
程序运行结束...
程序运行时间:501.5270709991455 毫秒
Geek-Mac:Downloads zhangyi$ 
补充:
  • 通过装饰器,我们可以在需要装饰器的函数上方加上 @语法糖 就可以,然后正常调用 mian 函数了

  • 程序中 @deco 和 main = deco(main) 是完全等价的,本质是一样的

  • 语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会

5. 装饰器(带参数)

因为装饰器本质也是函数,是函数就有参数,接下来看一个有参数的装饰器

#!/usr/bin/env python3
#-*-coding:UTF-8-*-

import time


def deco(userMain):
    def program_times(a, b):
        startTime = time.time()
        userMain(a, b)
        endTime = time.time()
        msecsTime = (endTime - startTime) * 1000
        print("程序运行时间:%s 毫秒" % msecsTime)
    return program_times

@deco
def main(a, b):
    print("程序运行开始...")
    time.sleep(0.5)
    print("a + b = %s " % (a + b))
    print("程序运行结束...")

print("此时 main 函数的名称是:%s " % main.__name__)

main(1, 2)

运行结果:

Geek-Mac:Downloads zhangyi$ python3 Decorator.py 
此时 main 函数的名称是:program_times 
程序运行开始...
a + b = 3 
程序运行结束...
程序运行时间:501.7970542907715 毫秒
Geek-Mac:Downloads zhangyi$ 
补充:
  • 从例子中可以看到,对于被装饰函数需要支持参数的情况,我们只要使装饰器的内嵌函数支持同样的签名即可

  • 例子中 main(1, 2) 其实是 deco(main(1, 2)) 的形式,先把 main 作为参数传给 deco 返回一个 program_times,之后再给 program_times 赋值

5. 装饰器(可变参数)

上面例子还有两个问题:

1. 如果多个函数拥有不同的参数形式,怎么共用同样的装饰器?

在 Python 中,函数可以支持 (*args, **kwargs) 可变参数,所以装饰器可以通过可变参数形式来实现内嵌函数的签名

2. 如果装饰器本身需要支持参数,怎么实现?

如果装饰器本身需要支持参数,那么装饰器就需要多一层的内嵌函数

#!/usr/bin/env python3
#-*-coding:UTF-8-*-

import time


def deco(text):
    def _deco(userMain):
        def program_times(*args, **kwargs):
            print(text)
            startTime = time.time()
            userMain(*args, **kwargs)
            endTime = time.time()
            msecsTime = (endTime - startTime) * 1000
            print("程序运行时间:%s 毫秒" % msecsTime)
        return program_times
    return _deco


@deco("这个是 deco 的参数")
def main(a, b, c):
    print("程序运行开始...")
    time.sleep(0.5)
    print("a + b + c = %s " % (a + b + c))
    print("程序运行结束...")

print("此时 main 函数的名称是:%s " % main.__name__)

main(1, 2, 3)

运行结果:

Geek-Mac:Downloads zhangyi$ python3 Decorator.py 
此时 main 函数的名称是:program_times 
这个是 deco 的参数
程序运行开始...
a + b + c = 6 
程序运行结束...
程序运行时间:504.67705726623535 毫秒
Geek-Mac:Downloads zhangyi$
补充:
  • 例子中 main(1, 2, 3) 其实是 deco("这个是 deco 的参数")(main(1, 2,3)) 的形式,首先执行 deco("这个是 deco 的参数") 返回的是 _deco 函数,再调用返回的 _deco 函数,参数是 main 函数,返回最终的 program_times 函数
6. 装饰器调用顺序

装饰器是可以叠加使用的,那么这是就涉及到装饰器调用顺序了。对于 Python 中的 @ 语法糖,装饰器的调用顺序与使用 @ 语法糖声明的顺序相反

#!/usr/bin/env python3
#-*-coding:UTF-8-*-

import time


def deco_1(userMain):
    def program_times(*args, **kwargs):
        startTime = time.time()
        userMain(*args, **kwargs)
        endTime = time.time()
        msecsTime = (endTime - startTime) * 1000
        print("这里是 @deco_1 ")
        print("程序运行时间:%s 毫秒" % msecsTime)
    return program_times

def deco_2(userMain):
    def program_times(*args, **kwargs):
        startTime = time.time()
        userMain(*args, **kwargs)
        endTime = time.time()
        msecsTime = (endTime - startTime) * 1000
        print("这里是 @deco_2 ")
        print("程序运行时间:%s 毫秒" % msecsTime)
    return program_times

@deco_1
@deco_2
def main(a, b):
    print("程序运行开始...")
    time.sleep(0.5)
    print("a + b = %s " % (a + b))
    print("程序运行结束...")

main(1, 2)

运行结果:

Geek-Mac:Downloads zhangyi$ python3 Decorator.py 
程序运行开始...
a + b = 3 
程序运行结束...
这里是 @deco_2 
程序运行时间:505.2931308746338 毫秒
这里是 @deco_1 
程序运行时间:505.4490566253662 毫秒
Geek-Mac:Downloads zhangyi$
补充:
  • 例子中 main(1, 2) 其实是 deco1(deco2(main(1, 2))) 的形式,所以先执行 @deco_2
7. 内置装饰器
在 Python 中有三个内置的装饰器,都是跟 class 相关的: staticmethod 、classmethod 、 property
  • staticmethod 是类静态方法,其跟成员方法的区别是没有 self 参数,并且可以在类不进行实例化的情况下调用

  • classmethod 与成员方法的区别在于所接收的第一个参数不是 self (类实例的指针),而是cls(当前类的具体类型)

  • property 是属性的意思,表示可以通过通过类实例直接访问的信息

对于 staticmethod 和 classmethod 这里就不介绍了,通过一个例子看看 property

#!/usr/bin/env python3
#-*-coding:UTF-8-*-

class Deco(object):

    def __init__(self, var):
        super(Deco, self).__init__()
        self._var = var

    @property
    def var(self):
        return self._var

    @var.setter
    def var(self, var):
        self._var = var

deco = Deco("Deco 1")
print(deco.var)
deco.var = "Deco 2"
print(deco.var)

运行结果:

Geek-Mac:Downloads zhangyi$ python3 Decorator.py 
Deco 1
Deco 2
Geek-Mac:Downloads zhangyi$ 
补充:
  • 注意,对于 Python 新式类(new-style class),如果将上面的 @var.setter 装饰器所装饰的成员函数去掉,则 Foo.var 属性为只读属性,使用 foo.var = 'var 2' 进行赋值时会抛出异常。但是,对于 Python classic class,所声明的属性不是 read-only 的,所以即使去掉 @var.setter 装饰器也不会报错。

参考文章:http://python.jobbole.com/82344/


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值