|
|
51CTO旗下网站
|
|
移动端

2.2.3 装饰器(1)

《Python高级编程(第2版)》第2章语法最佳实践——类级别以下,本章将介绍现在这门语言的语法中最重要的元素,以及它们的使用技巧。本节为大家介绍装饰器。

作者:张亮/阿信 译来源:人民邮电出版社|2018-01-29 18:26

2.2.3 装饰器(1)

Python装饰器的作用是使函数包装与方法包装(一个函数,接受函数并返回其增强函数)变得更容易阅读和理解。最初的使用场景是在方法定义的开头能够将其定义为类方法或静态方法。如果不用装饰器语法的话,定义可能会非常稀疏,并且不断重复:

  1. class WithoutDecorators:  
  2.     def some_static_method():  
  3.         print("this is static method")  
  4.     some_static_method = staticmethod(some_static_method)  
  5.     def some_class_method(cls):  
  6.         print("this is class method")  
  7.     some_class_method = classmethod(some_class_method) 

如果用装饰器语法重写的话,代码会更简短,也更容易理解:

  1. class WithDecorators:  
  2.     @staticmethod  
  3.     def some_static_method():  
  4.         print("this is static method")  
  5.  
  6.     @classmethod  
  7.     def some_class_method(cls):  
  8.         print("this is class method") 

1.一般语法和可能的实现

装饰器通常是一个命名的对象(不允许使用lambda表达式),在被(装饰函数)调用时接受单一参数,并返回另一个可调用对象。这里用的是“可调用(callable)”。而不是之前以为的“函数”。装饰器通常在方法和函数的范围内进行讨论,但它的适用范围并不局限于此。事实上,任何可调用对象(任何实现了__ call __方法的对象都是可调用的)都可以用作装饰器,它们返回的对象往往也不是简单的函数,而是实现了自己的__ call __方法的更复杂的类的实例。

装饰器语法只是语法糖而已。看下面这种装饰器用法:

  1. @some_decorator  
  2. def decorated_function():  
  3.     pass 

这种写法总是可以替换为显式的装饰器调用和函数的重新赋值:

  1. def decorated_function():  
  2.     pass  
  3. decorated_function = some_decorator(decorated_function) 

但是,如果在一个函数上使用多个装饰器的话,后一种写法的可读性更差,也非常难以理解。

装饰器甚至不需要返回可调用对象! 

事实上,任何函数都可以用作装饰器,因为Python并没有规定装饰器的返回类型。因此,将接受单一参数但不返回可调用对象的函数(例如str)用作装饰器,在语法上是完全有效的。如果用户尝试调用这样装饰过的对象,最后终究会报错。不管怎样,针对这种装饰器语法可以做一些有趣的试验。

(1)作为一个函数

编写自定义装饰器有许多方法,但最简单的方法就是编写一个函数,返回包装原始函数调用的一个子函数。

通用模式如下:

  1. def mydecorator(function):  
  2.     def wrapped(*args, **kwargs):  
  3.         # 在调用原始函数之前,做点什么  
  4.         result = function(*args, **kwargs)  
  5.         # 在函数调用之后,做点什么,  
  6.         # 并返回结果  
  7.         return result  
  8.     # 返回wrapper作为装饰函数  
  9.     return wrapped 

(2)作为一个类
      
虽然装饰器几乎总是可以用函数实现,但在某些情况下,使用用户自定义类可能更好。如果装饰器需要复杂的参数化或者依赖于特定状态,那么这种说法往往是对的。

非参数化装饰器用作类的通用模式如下:

  1. class DecoratorAsClass:  
  2.     def __init__(self, function):  
  3.         self.function = function  
  4.  
  5.     def __call__(self, *args, **kwargs):  
  6.         # 在调用原始函数之前,做点什么  
  7.         result = self.function(*args, **kwargs)  
  8.         # 在调用函数之后,做点什么,  
  9.         # 并返回结果  
  10.         return result 

(3)参数化装饰器

在实际代码中通常需要使用参数化的装饰器。如果用函数作为装饰器的话,那么解决方法很简单:需要用到第二层包装。下面一个简单的装饰器示例,给定重复次数,每次被调用时都会重复执行一个装饰函数:

  1. def repeat(number=3):  
  2.     """多次重复执行装饰函数。  
  3.  
  4.     返回最后一次原始函数调用的值作为结果  
  5.     :param number: 重复次数,默认值是3  
  6.     """  
  7.     def actual_decorator(function):  
  8.         def wrapper(*args, **kwargs):  
  9.             result = None 
  10.             for _ in range(number):  
  11.                 result = function(*args, **kwargs)  
  12.             return result  
  13.         return wrapper  
  14.     return actual_decorator 

这样定义的装饰器可以接受参数:

  1. >>> @repeat(2)  
  2. ... def foo():  
  3. ...     print("foo")  
  4. ...  
  5. >>> foo()  
  6. foo  
  7. foo 

注意,即使参数化装饰器的参数有默认值,但名字后面也必须加括号。带默认参数的装饰器的正确用法如下:

  1. >>> @repeat()  
  2. ... def bar():  
  3. ...     print("bar")  
  4. ...  
  5. >>> bar()  
  6. bar  
  7. bar  
  8. bar 

没加括号的话,在调用装饰函数时会出现以下错误:

  1. >>> @repeat  
  2. ... def bar():  
  3. ...     pass  
  4. ...  
  5. >>> bar()  
  6. Traceback (most recent call last):  
  7.   File "< input >", line 1, in < module > 
  8. TypeError: actual_decorator() missing 1 required positional  
  9. argument: 'function' 

(4)保存内省的装饰器

使用装饰器的常见错误是在使用装饰器时不保存函数元数据(主要是文档字符串和原始函数名)。前面所有示例都存在这个问题。装饰器组合创建了一个新函数,并返回一个新对象,但却完全没有考虑原始函数的标识。这将会使得调试这样装饰过的函数更加困难,也会破坏可能用到的大多数自动生成文档的工具,因为无法访问原始的文档字符串和函数签名。

但我们来看一下细节。假设我们有一个虚设的(dummy)装饰器,仅有装饰作用,还有其他一些被装饰的函数:

  1. def dummy_decorator(function):  
  2.     def wrapped(*args, **kwargs):  
  3.         """包装函数内部文档。"""  
  4.         return function(*args, **kwargs)  
  5.     return wrapped  
  6.  
  7. @dummy_decorator  
  8. def function_with_important_docstring():  
  9.     """这是我们想要保存的重要文档字符串。""" 

喜欢的朋友可以添加我们的微信账号:

51CTO读书频道二维码


51CTO读书频道活动讨论群:365934973

【责任编辑:book TEL:(010)68476606】

回书目   上一节   下一节
点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

UNIX到Linux的移植

本书讲述怎样把UNIX环境下的应用程序移植到Linux环境上运行,是一本综合的开发和解决问题的参考手册 。本书详细描述了当前IT行业中被广泛应...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊