先混个眼熟

谁可以作为装饰器(可以将谁编写成装饰器):

  1. 函数
  2. 方法
  3. 实现了__call__的可调用类

装饰器可以去装饰谁(谁可以被装饰):

  1. 函数
  2. 方法

基础:函数装饰器的表现方式

假如你已经定义了一个函数funcA(),在准备定义函数funcB()的时候,如果写成下面的格式:

@funcA
def funcB():...

表示用函数funcA()装饰函数funcB()。当然,也可以认为是funcA包装函数funcB。它等价于:

def funcB():...

funcB = funcA(funcB)

也就是说,将函数funcB作为函数funcA的参数,funcA会重新返回另一个可调用的对象(比如函数)并赋值给funcB。

所以,funcA要想作为函数装饰器,需要接收函数作为参数,并且返回另一个可调用对象(如函数)。例如:

def funcA(F):
...
...
return Callable

注意,函数装饰器返回的可调用对象并不一定是原始的函数F,可以是任意其它可调用对象,比如另一个函数。但最终,这个返回的可调用对象都会被赋值给被装饰的函数变量(上例中的funcB)。

函数可以同时被多个装饰器装饰,后面的装饰器以前面的装饰器处理结果为基础进行处理:

@decorator1
@decorator2
def func():... # 等价于
func = decorator1(decorator2(func))

当调用被装饰后的funcB时,将自动将funcB进行装饰,并调用装饰后的对象。所以,下面是等价的调用方式:

funcB()          # 调用装饰后的funcB
funcA(funcB)()

了解完函数装饰器的表现后,大概也能猜到了,装饰器函数可以用来扩展、增强另外一个函数。实际上,内置函数中staticmethod()、classmethod()和property()都是装饰器函数,可以用来装饰其它函数,在后面会学到它们的用法。

两个简单的例子

例如,函数f()返回一些字符串,现在要将它的返回结果转换为大写字母。可以定义一个函数装饰器来增强函数f()。

def toupper(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper @toupper
def f(x: str): # 等价于f = toupper(f)
return x res = f("abcd")
print(res)

上面toupper()装饰f()后,调用f("abcd")的时候,等价于执行toupper(f)("abcd"),参数"abcd"传递给装饰器中的wrapper()中的*args,在wrapper中又执行了f("abcd"),使得原本属于f()的整个过程都完整了,最后返回result.upper(),这部分是对函数f()的扩展部分。

注意,上面的封装函数wrapper()中使用了*args **kwargs,是为了确保任意参数的函数都能正确执行下去。

再比如要计算一个函数autodown()的执行时长,可以额外定义一个函数装饰器timecount()。

import time

# 函数装饰器
def timecount(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end - start)
return result
return wrapper # 装饰函数
@timecount
def autodown(n: int):
while n > 0:
n -= 1 # 调用被装饰的函数
autodown(100000)
autodown(1000000)
autodown(10000000)

执行结果:

autodown 0.004986763000488281
autodown 0.05684685707092285
autodown 0.5336081981658936

上面wrapper()中的return是多余的,是因为这里装饰的autodown()函数自身没有返回值。但却不应该省略这个return,因为timecount()可以去装饰其它可能有返回值的函数。

@functools.wraps

前面的装饰器代码逻辑上没有什么问题,但是却存在隐藏的问题:函数的元数据信息丢了。比如doc、注解等。

比如下面的代码:

@timecount
def autodown(n: int):
''' some docs '''
while n > 0:
n -= 1 print(autodown.__name__)
print(autodown.__doc__)
print(autodown.__annotations__)

执行结果为:

wrapper
None
{}

所以,必须要将被装饰函数的元数据保留下来。可以使用functools模块中的wraps()装饰一下装饰器中的wrapper()函数。如下:

import time
from functools import wraps def timecount(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end - start)
return result
return wrapper

现在,再去查看autodown函数的元数据信息,将会得到被保留下来的内容:

autodown
some doc
{'n': <class 'int'>}

所以,wraps()的简单用法是:向wraps()中传递的func参数,那么func的元数据就会被保留下来。

上面@wraps(func)装饰wrapper的过程等价于:

def wrapper(*args, **kwargs):...
wrapper = wraps(func)(wrapper)

请注意这一点,因为在将类作为装饰器的时候,经常会在__init__(self, func)里这样使用:

class cls:
def __init__(self, func):
wraps(func)(self)
...
def __call__(self, *args, **kwargs):
...

解除装饰

函数被装饰后,如何再去访问未被装饰状态下的这个函数?@wraps还有一个重要的特性,可以通过被装饰对象的__wrapped__属性来直接访问被装饰对象。例如:

autodown.__wrapped__(1000000)

new_autodown = autodown.__wrapped__
new_autodown(1000000)

上面的调用不会去调用装饰后的函数,所以不会输出执行时长。

注意,如果函数被多个装饰器装饰,那么通过__wrapped__,将只会解除第一个装饰过程。例如:

@decorator1
@decorator2
@decorator3
def f():...

当访问f.__wrapped__()的时候,只有decorator1被解除,剩余的所有装饰器仍然有效。注意,python 3.3之前是略过所有装饰器。

下面是一个多装饰的示例:

from functools import wraps

def decorator1(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("in decorator1")
return func(*args, **kwargs)
return wrapper def decorator2(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("in decorator2")
return func(*args, **kwargs)
return wrapper def decorator3(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("in decorator3")
return func(*args, **kwargs)
return wrapper @decorator1
@decorator2
@decorator3
def addNum(x, y):
return x+y

返回结果:

in decorator1
in decorator2
in decorator3
5
in decorator2
in decorator3
5

如果不使用functools的@wraps的__wrapped__,想要手动去引用原始函数,需要做的工作可能会非常多。所以,如有需要,直接使用__wrapped__去调用未被装饰的函数比较好。

另外,并不是所有装饰器中都使用了@wraps

带参数的函数装饰器

函数装饰器也是可以带上参数的。

@decorator(x,y,z)
def func():...

它等价于:

func = decorator(x,y,z)(func)

它并不是"天生"就这样等价的,而是根据编码规范编写装饰器的时候,通常会这样。其实带参数的函数装饰器写起来有点绕:先定义一个带有参数的外层函数,它是外在的函数装饰器,这个函数内包含了真正的装饰器函数,而这个内部的函数装饰器的内部又包含了被装饰的函数封装。也就是函数嵌套了一次又一次。

所以,结构大概是这样的:

def out_decorator(some_args):
...SOME CODE...
def real_decorator(func):
...SOME CODE...
def wrapper(*args, **kwargs):
...SOME CODE WITH func...
return wrapper
return real_decorator # 等价于func = out_decorator(some_args)(func)
@out_decorator(some_args)
def func():...

下面是一个简单的例子:

from functools import wraps

def out_decorator(x, y, z):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(x)
print(y)
print(z)
return func(*args, **kwargs)
return wrapper
return decorator @out_decorator("xx", "yy", "zz")
def addNum(x, y):
return x+y print(addNum(2, 3))

参数随意的装饰器

根据前面介绍的两种情况,装饰器可以带参数、不带参数,所以有两种装饰的方式,要么是下面的(1),要么是下面的(2)。

@decorator         # (1)
@decorator(x,y,z) # (2)

所以,根据不同的装饰方式,需要编写是否带参数的不同装饰器。

但是现在想要编写一个将上面两种参数方式统一起来的装饰器。

可能第一想法是让装饰器参数默认化:

def out_decorator(arg1=X, arg2=Y...):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
...
return wrapper
return decorator

现在可以用下面两种方式来装饰:

@out_decorator()
@out_decorator(arg1,arg2)

虽然上面两种装饰方式会正确进行,但这并非合理做法,因为下面这种最通用的装饰方式会错误:

@out_decorator

为了解决这个问题,回顾下前面装饰器是如何等价的:

# 等价于 func = decorator(func)
@decorator
def func():... # 等价于 func = out_decorator(x, y, z)(func)
@out_decorator(x, y, z)
def func():...

上面第二种方式中,out_decorator(x,y,z)才是真正返回的内部装饰器。所以,可以修改下装饰器的编写方式,将func也作为out_decorator()的其中一个参数:

from functools import wraps,partial

def decorator(func=None, arg1=X, arg2=Y):
# 如果func为None,说明触发的带参装饰器
# 直接返回partial()封装后的装饰器函数
if func is None:
decorator_new = partial(decorator, arg1=arg1, arg2=arg2)
return decorator_new
#return partial(decorator, arg1=arg1, arg2=arg2) # 下面是装饰器的完整装饰内容
@wraps(func)
def wrapper(*args, **kwargs):
...
return wrapper

上面使用了functools模块中的partial()函数,它可以返回一个新的将某些参数"冻结"后的函数,使得新的函数无需指定这些已被"冻结"的参数,从而减少参数的数量。如果不知道这个函数,参考partial()用法说明

现在,可以统一下面3种装饰方式:

@decorator()
@decorator(arg1=x,arg2=y)
@decorator

前两种装饰方式,等价的调用方式是decorator()(func)decorator(arg1=x,arg2=y)(func),它们的func都为None,所以都会通过partial()返回通常的装饰方式@decorator所等价的形式。

需要注意的是,因为上面的参数结构中包含了func=None作为第一个参数,所以带参数装饰时,必须使用keyword格式来传递参数,不能使用位置参数。

下面是一个简单的示例:

from functools import wraps, partial

def decorator(func=None, x=1, y=2, z=3):
if func is None:
return partial(decorator, x=x, y=y, z=z) @wraps(func)
def wrapper(*args, **kwargs):
print("x: ", x)
print("y: ", y)
print("z: ", z)
return func(*args, **kwargs)
return wrapper

下面3种装饰方式都可以:

@decorator
def addNum(a, b):
return a + b
print(addNum(2, 3)) print("=" * 40) @decorator()
def addNum(a, b):
return a + b
print(addNum(2, 3)) print("=" * 40) # 必须使用关键字参数进行装饰
@decorator(x="xx", y="yy", z="zz")
def addNum(a, b):
return a + b
print(addNum(2, 3))

返回结果:

x:  1
y: 2
z: 3
5
====================
x: 1
y: 2
z: 3
5
====================
x: xx
y: yy
z: zz
5

python装饰器1:函数装饰器详解的更多相关文章

  1. Python学习入门基础教程(learning Python)--2.3.3Python函数型参详解

    本节讨论Python下函数型参的预设值问题. Python在设计函数时,可以给型参预设缺省值,当用户调用函数时可以不输入实参.如果用户不想使用缺省预设值则需要给型参一一赋值,可以给某些型参赋值或不按型 ...

  2. Python学习笔记:函数和变量详解

    一.面向对象:将客观世界的事物抽象成计算机中的数据结构 类:用class定义,这是当前编程的重点范式,以后会单独介绍. 二.函数编程:逻辑结构化和过程化的一种编程方法 1.函数-->用def定义 ...

  3. 【转】Python的hasattr() getattr() setattr() 函数使用方法详解

    Python的hasattr() getattr() setattr() 函数使用方法详解 hasattr(object, name)判断一个对象里面是否有name属性或者name方法,返回BOOL值 ...

  4. Python中操作mysql的pymysql模块详解

    Python中操作mysql的pymysql模块详解 前言 pymsql是Python中操作MySQL的模块,其使用方法和MySQLdb几乎相同.但目前pymysql支持python3.x而后者不支持 ...

  5. c++中内存拷贝函数(C++ memcpy)详解

    原型:void*memcpy(void*dest, const void*src,unsigned int count); 功能:由src所指内存区域复制count个字节到dest所指内存区域. 说明 ...

  6. Python调用C/C++动态链接库的方法详解

    Python调用C/C++动态链接库的方法详解 投稿:shichen2014 这篇文章主要介绍了Python调用C/C++动态链接库的方法,需要的朋友可以参考下 本文以实例讲解了Python调用C/C ...

  7. Python中__init__.py文件的作用详解

    转自http://www.jb51.net/article/92863.htm Python中__init__.py文件的作用详解 http://www.jb51.net/article/86580. ...

  8. 基于python中staticmethod和classmethod的区别(详解)

    例子 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 class A(object):   def foo(self,x):     print "executing foo ...

  9. Python中的__name__和__main__含义详解

    1背景 在写Python代码和看Python代码时,我们常常可以看到这样的代码: ? 1 2 3 4 5 def main():     ......   if __name == "__m ...

  10. 【python】redis基本命令和基本用法详解

    [python]redis基本命令和基本用法详解 来自http://www.cnblogs.com/wangtp/p/5636872.html 1.redis连接 redis-py提供两个类Redis ...

随机推荐

  1. Waiting for table metadata lock

    出现下图这个现象之前是在一张事务操作频繁地表上,执行了truncate操作. mysql.sock@(none)> select user,host,db,command,time,state, ...

  2. 【慕课网实战】九、以慕课网日志分析为例 进入大数据 Spark SQL 的世界

    即席查询普通查询 Load Data1) RDD DataFrame/Dataset2) Local Cloud(HDFS/S3) 将数据加载成RDDval masterLog = sc.textFi ...

  3. Codeforces Round #485 (Div. 2) E. Petr and Permutations

    Codeforces Round #485 (Div. 2) E. Petr and Permutations 题目连接: http://codeforces.com/contest/987/prob ...

  4. spring扩展点总结

    NamespaceHandler 通过自定义的NamespaceHandler,配合BeanDefinitionParser,可以完成自定义Bean的组装操作,对于BeanDefinition的数据结 ...

  5. python之路(八)-迭代器&生成器

    迭代器 迭代器是访问集合元素的一种方式.迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束.迭代器只能往前不会后退,不过这也没什么,因为人们很少在迭代途中往后退.另外,迭代器的一大优点是 ...

  6. windows putty xming virt-manager

    记一次windows环境使用linux下使用virt-manager软件的问题 环境:windows server 2008.ubuntu-server 软件:putty.virt-manager.x ...

  7. 9-Unittest+HTMLTestRunner不能生成报告解决方法

    1.问题现象 在使用HTMLTestRunner生成测试报告时,出现程序运行不报错,但不能生成报告的情况. 刚开始找了很久没发现问题,后来加上打印信息,发现根本没执行生成报告这部分代码.最后网上找到原 ...

  8. spring 排除指定的类或者包扫描

    <!-- 排除Controller注解的扫描 --> <context:component-scan base-package="exampleBean"> ...

  9. 包建强的培训课程(11):iOS Runtime实战

    @import url(http://i.cnblogs.com/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/c ...

  10. vue组件推荐

    Vue 是一个轻巧.高性能.可组件化的MVVM库,API简洁明了,上手快.从Vue推出以来,得到众多Web开发者的认可.在公司的Web前端项目开发中,多个项目采用基于Vue的UI组件框架开发,并投入正 ...