当前位置: 首页 > 文档资料 > Python 设计模式 >

装饰器模式

优质
小牛编辑
128浏览
2023-12-01

Whenever we want to add extra functionality to an object, we have a number of different options. We can:

任何时候,只要我们想给一个对象添加附加的功能,就有多个不同的选项供我们选择。我们可以选择:

  • Add the functionality directly to the class the object belongs to, if it makes sense (for example, add a new method)
  • Use composition
  • Use inheritance

  • 直接对类的对象添加功能,如果可行的话(例如,添加一个新方法)

  • 合成
  • 继承

Composition should generally be preferred over inheritance, because inheritance makes code reuse harder, it's static, and applies to an entire class and all instances of it [GOF95, page 31], [j.mp/decopat].

通常对合成的选择应该优先于继承,因为继承会使代码很难重复使用,而且继承是静态的,它会被应用到整个类,以及类的全部实例。

Design patterns offer us a fourth option that supports extending the functionality of an object dynamically (in runtime): Decorators. A Decorator pattern can add responsibilities to an object dynamically, and in a transparent manner (without affecting other objects) [GOF95, page 196].

设计模式给我们动态地(运行时)提供了扩展一个对象的第四个选择:装饰器。装饰器模式能够动态地给一个对象添加信任,而且是使用透明模式(不会影响到其他的对象)。

In many programming languages, the Decorator pattern is implemented using sub-classing (inheritance) [GOF95, page 198]. In Python, we can (and should) use the built-in decorator feature. A Python decorator is a specific change to the syntax of Python that is used for extending the behavior of a class, method, or function without using inheritance. In terms of implementation, a Python decorator is a callable (function, method, class) that accepts a function object fin as input, and returns another function object fout [j.mp/conqdec]. This means that any callable that has these properties can be treated as a decorator. We have already seen how to use the built-in property decorator that makes a method appear as a variable in Chapter 1, The Factory Pattern and Chapter 2, The Builder Pattern. In the implementation section, we will learn how to implement and use our own decorators.

在很多的编程语言中,装饰器模式是通过使用子类化(继承)实现的[GOF95,198页]。在Python中我们可以(而且应该)使用内建的装饰器功能。Python装饰器是专门用来改变Python语法的,它用来扩展一个类的行为,方法,或者函数不需要用到继承。就实现的术语观点来看,Python装饰器是一个可调用的对象(函数、方法、类)能够接受函数对象作为输入,并返回另外一个不同的函数。这就意味着任何一个拥有这些特性的可调用对象都可以被当作装饰器。我们已经在第一章《工厂模式》和第二章《构造器模式》见过了如何使用内建的特性装饰器将一个方法以变量的形式出现。在具体实现部分,我们会学习到如何实现并利用装饰器。

There is no one-to-one relationship between the Decorator pattern and Python decorators. Python decorators can actually do much more than the Decorator pattern. One of the things they can be used for, is to implement the Decorator pattern [Eckel08, page 59], [j.mp/moinpydec].

Python的装饰器和装饰器模式之间不存在一对一的关系。Python装饰器实际上能做的事情比装饰器模式多得多。使用Python装饰器的其中一个目的就是实现装饰器模式[Eckel08, 59页]。

真实事例

The fact that the pattern is called Decorator does not mean that it should be used only for making things look prettier. The Decorator pattern is generally used for extending the functionality of an object. Real examples of such extensions are: adding a silencer to a gun, using different camera lenses (in cameras with removable lenses), and so on.

模式被称做装饰器实际上并不意味着它只应该让事情更好些。通常装饰器模式用来扩展一个对象的功能。这样的扩展的真实例子是:给一把枪添加一个消音器,使用不同的相机镜头(可移除的镜头),等等。

The following gure, provided by sourcemaking.com, shows how we can decorate a gun with special accessories to make it silent, more accurate, and devastating [j.mp/ decopat]. Note that the gure uses sub-classing, but in Python, this is not necessary because we can use the built-in decorator feature of the language.

sourcemaking.com提供的下图,展示了我们如何利用特殊的部件来装饰一把枪以使枪消音,更加精确,具有杀伤性。注意此图使用的是子类化,不过在Python中,这不是必须的,因为我们可以利用该语言的内建装饰器功能。

img

软件示例

The Django framework uses decorators to a great extent. An example is the View decorator. Django's View decorators can be used for [j.mp/djangodec]:

Django框架使用装饰器来实现良好的扩展。例子就是视图装饰器。Django的视图装饰器可以用于:

  • Restricting access to views based on the HTTP request
  • Controlling the caching behavior on speci c views
  • Controlling compression on a per-view basis
  • Controlling caching based on speci c HTTP request headers

  • 限制基于HTTP请求的视图

  • 控制特定视图的缓存行为
  • 控制每个视图基础的压缩
  • 控制依据特定HTTP请求偷渡的缓存

The Grok framework also uses decorators for achieving different goals such as [j.mp/grokdeco]:

Grok框架也使用装饰器来实现不同的目标:

  • Registering a function as an event subscriber
  • Protecting a method with a speci c permission
  • Implementing the Adapter pattern

  • 函数注册为一个时间订阅器

  • 对方法应用一个特定的权限
  • 实现适配器模式

使用案例

The Decorator pattern shines when used for implementing cross-cutting concerns [Lott14, page 223], [j.mp/wikicrosscut]. Examples of cross-cutting concerns are:

装饰器模式用于实现横切关注时特别出色。横切关注的例子有:

  • Data validation
  • Transaction processing (A transaction in this case is similar to a database transaction, in the sense that either all steps should be completed successfully, or the transaction should fail.)
  • Caching
  • Logging
  • Monitoring
  • Debugging
  • Business rules
  • Compression
  • Encryption

  • 数据验证

  • 事物处理(这种场景下的事物类似于数据库事物,感觉上所有的步骤要么都应该是成功,要么事物应该失败。)
  • 缓存
  • 登录
  • 监控
  • 调试
  • 业务规则
  • 压缩
  • 加密

In general, all parts of an application that are generic and can be applied to many other parts of it, are considered cross-cutting concerns.

通常来说,一个应用的所有部分都是通用的,而且能够被运用于该应用的其它部分,就被认为是关注切面的。

Another popular example of using the Decorator pattern is Graphical User Interface (GUI) toolkits. In a GUI toolkit, we want to be able to add features such as borders, shadows, colors, and scrolling to individual components/widgets [GOF95, page 196].

另外一个流行的使用装饰器模式的例子是图形化用户界面(GUI)工具套件。在GUI工具套件中,我们想要能够独立的对组件/部件添加诸如,边框,阴影,色彩,以及滚动。

实现

Python decorators are generic and very powerful. You can nd many examples of how they can be used at the decorator library of python.org [j.mp/pydeclib]. In this section, we will see how we can implement a memoization decorator [j.mp/ memoi]. All recursive functions can bene t from memoization, so let's pick the popular Fibonacci sequence example. Implementing the recursive algorithm of Fibonacci is straight forward, but it has major performance issues, even for small values. First, let's see the naive implementation ( le fibonacci_naive.py).

Python装饰器通用而且非常强大。你可以在python.org上的装饰器库找到很多如何使用的例子。在本节,我们会看到如何如何实现一个记忆器装饰器。所有的递归函数都可以从记忆器中获益,所以让我们选择非常流行的斐波那契序列示例。实现斐波那契的递归算法简单明了,但是存在重大的性能问题,即便是对于很小的值。首先,让我们来看一看原始实现(文件fibonacci_naive.py)。

 def fibonacci(n):
       assert(n >= 0), 'n must be >= 0'
       return n if n in (0, 1) else fibonacci(n-1) + fibonacci(n-2)
   if __name__ == '__main__':
       from timeit import Timer
       t = Timer('fibonacci(8)', 'from __main__ import fibonacci')
       print(t.timeit())

A sample execution of this example shows how slow this implementation is. It takes 17 seconds to calculate the eighth Fibonacci number. The same execution gives the following output:

该例子的执行显示了此种实现是如何的慢。它花了17秒计算8个斐波那契数。相同的执行给出如下输出:

>>> python3 fibonacci_naive.py
16.669270177000726

Let's use memoization to see if it helps. In the following code, we use a dict for caching the already computed values of the Fibonacci sequence. We also change the parameter passed to the fibonacci() function. We want to calculate the hundredth Fibonacci number instead of the eighth.

我们使用记忆器来看看是否有些帮助。在下面的代码中,我们使用字典缓存斐波那契序列已经计算过的值。我们还改变了传递到fibonacci()函数的参数。我们想要计算上百个斐波那契数而不是区区八个。

known = {0:0, 1:1}


def fibonacci(n):
   assert(n >= 0), 'n must be >= 0'
   if n in known:
       return known[n]
   res = fibonacci(n-1) + fibonacci(n-2)
   known[n] = res
    return res

if __name__ == '__main__':
    from timeit import Timer
    t = Timer('fibonacci(100)', 'from __main__ import fibonacci') print(t.timeit())

Executing the memoization-based code shows that performance improves dramatically, and is acceptable even for computing large values. A sample execution is as follows:

执行基于记忆器的代码显示了性能戏剧性地提升了,而且甚至可以计算较大的值。样例执行如下:

>>> python3 fibonacci.py
0.31532211999729043

But there are already a few problems with this approach. While the performance is not an issue any longer, the code is not as clean as it is when not using memoization. And what happens if we decide to extend the code with more math functions and turn it into a module? Let's assume that the next function we decide to add is nsum(), which returns the sum of the rst n numbers. Note that this function is already available in the math module as fsum(), but we can easily think of other functions that are not available in the standard library and would be useful for our module (for example Pascal's triangle, the sieve of Eratosthenes, and so on). The code of the nsum() function using memoization ( le mymath.py) is given as follows:

但这种方式存在已知的几个问题。当性能不再是问题时,代码没能够像未使用记忆器时那样简洁。如果我们决定用更多的math函数来扩展,并把它加入到模块中会发生什么呢?我们假设接下来的函数我们决定添加的是nsum(),它放了第一个n数字的总和。注意这个函数

   known_sum = {0:0}
   def nsum(n):
       assert(n >= 0), 'n must be >= 0'
       if n in known_sum:
           return known_sum[n]
       res = n + nsum(n-1)
       known_sum[n] = res
       return res

Do you notice the problem already? We ended up with a new dict called known_sum which acts as our cache for nsum, and a function that is more complex than it would be without using memoization. Our module is becoming unnecessarily complex. Is it possible to keep the recursive functions as simple as the naive versions, but achieve a performance similar to the performance of the functions that use memoization? Fortunately, it is, and the solution is to use the Decorator pattern.

你注意到了么已知的问题么?我们以一个称作known_sum新字典结束,它作为nsum的缓存器,而且函数比起没有使用记忆器的更为复杂。我们的模块正在变的无必要的复杂。是否有可能让递归函数和它的原始版本一样的简单,而获得的性能类似于使用了记忆器函数的性能?幸运的是,答案是肯定的,解决方案是使用装饰器模式。

First, we create a memoize() decorator as shown in the following example. Our decorator accepts the function fn that needs to be memoized, as an input. It uses a dict named known as the cache. The functools.wraps() function is a function that is used for convenience when creating decorators. It is not mandatory but a good practice to use since it makes sure that the documentation and the signature of the function that is decorated, are preserved [j.mp/funcwraps]. The argument list *args, is required in this case because the functions that we want to decorate accept input arguments. It would be redundant to use it if fibonacci() and nsum() didn't require any arguments, but they require n.

首先,我们创建展示在下面例子中的memoize()装饰器。我们的装饰器接受需要被记住的函数fn,以作为输入。它使用被称作cacha的字典。functools.wraps()函数是一个在创建装饰器时

   import functools
   def memoize(fn):
       known = dict()
       @functools.wraps(fn)
       def memoizer(*args):
           if args not in known:
               known[args] = fn(*args)
           return known[args]
       return memoizer

Now, we can use our memoize() decorator with the naive version of our functions. This has the benefit of readable code without performance impacts. We apply a decorator using what is known as decoration (or decoration line). A decoration uses the @name syntax, where name is the name of the decorator that we want to use. It is nothing more than syntactic sugar for simplifying the usage of decorators. We can even bypass this syntax and execute our decorator manually, but that is left as an exercise for you. Let's see how the memoize() decorator is used with our recursive functions in the following example:

现在,我们可以对函数的原始版本使用memoize()函数了。这可以带来代码可阅读伤的好处,而不影响到性能。

   @memoize
   def nsum(n):
       '''Returns the sum of the first n numbers'''
       assert(n >= 0), 'n must be >= 0'
       return 0 if n == 0 else n + nsum(n-1)


   @memoize
   def fibonacci(n):
       '''Returns the nth number of the Fibonacci sequence'''
       assert(n >= 0), 'n must be >= 0'
       return n if n in (0, 1) else fibonacci(n-1) + fibonacci(n-2)

The last part of the code shows how to use the decorated functions and measure their performance. measure is a list of dict used to avoid code repetition. Note how __name__ and __doc__ show the proper function names and documentation values, respectively. Try removing the @functools.wraps(fn) decoration from memoize(), and see if this is still the case:

代码的最后一部分展示了如何使用被装饰的函数来测量函数的性能。测量是一个避免代码重复的字典列表。注意,__name____doc__如何分别正确的显示函数名和文档的值。试着从memoize()移除@functools.wraps(fn)装饰器,看一看是否还是同样的结果:

   if __name__ == '__main__':
       from timeit import Timer
       measure = [ {'exec':'fibonacci(100)', 'import':'fibonacci',
       'func':fibonacci},{'exec':'nsum(200)', 'import':'nsum',
       'func':nsum} ]
       for m in measure:
           t = Timer('{}'.format(m['exec']), 'from __main__ import {}'.format(m['import']))
           print('name: {}, doc: {}, executing: {}, time: {}'.format(m['func'].__name__, m['func'].__doc__,
           m['exec'], t.timeit()))

Let's see the complete code of our math module ( le mymath.py) and a sample output when executing it.

我们来看看math模块的完整代码(文件mymath.py),以及在执行时的样例输出。

   import functools


   def memoize(fn):
       known = dict()
       @functools.wraps(fn)
       def memoizer(*args):
           if args not in known:
               known[args] = fn(*args)
           return known[args]
       return memoizer


   @memoize
   def nsum(n):
       '''Returns the sum of the first n numbers'''
       assert(n >= 0), 'n must be >= 0'
       return 0 if n == 0 else n + nsum(n-1)


   @memoize
   def fibonacci(n):
       '''Returns the nth number of the Fibonacci sequence'''
       assert(n >= 0), 'n must be >= 0'
       return n if n in (0, 1) else fibonacci(n-1) + fibonacci(n-2)


if __name__ == '__main__':
       from timeit import Timer
       measure = [ {'exec':'fibonacci(100)', 'import':'fibonacci',
       'func':fibonacci}, {'exec':'nsum(200)', 'import':'nsum',
       'func':nsum} ]
       for m in measure:
           t = Timer('{}'.format(m['exec']), 'from __main__ import{}'.format(m['import']))
           print('name: {}, doc: {}, executing: {}, time: {}'.format(m['func'].__name__, m['func'].__doc__,
           m['exec'], t.timeit()))

Note that the execution times might differ in your case.

注意,执行时间可能和你的有所不同。

>>> python3 mymath.py
name: fibonacci, doc: Returns the nth number of the Fibonacci
sequence, executing: fibonacci(100), time: 0.4169441329995607
name: nsum, doc: Returns the sum of the first n numbers,
executing: nsum(200), time: 0.4160157349997462

Nice. Readable code and acceptable performance. Now, you might argue that this is not the Decorator pattern, since we don't apply it in runtime. The truth is that a decorated function cannot be undecorated; but you can still decide in runtime if the decorator will be executed or not. That's an interesting exercise left for you.

非常好。可阅读的代码,加上可接受的性能。现在,你或许要同我争论,这不在是装饰器模式了,因为我们在运行时没有使用它。真相是被装饰器过的函数不能够被取消装饰;但在运行时你仍旧可以决定装饰器是否要执行。这就是留给你的一个有趣的练习。

Tips

Hint: Use a decorator that acts as a wrapper which decides whether or not the real decorator is executed based on some condition.

提示

线索:使用装饰器作为包装器,取决于真实的装饰器是否是按条件执行的。

Another interesting property of decorators that is not covered in this chapter is that, you can decorate a function with more than one decorator. So here's another exercise: create a decorator that helps you to debug recursive functions, and apply it on nsum() and fibonacci(). In what order are the multiple decorators executed?

装饰器的另外一个有趣的属性并不在本章讨论范围之内,即,你可以对一个函数应用不止一个装饰器。所以这里还有另外一个练习:创建一个帮助你调试递归函数的装饰器,并把它运用到nsum() 和 fibonacci()。其中多个装饰器的执行顺序是什么?

If you have not had enough with decorators, I have one last exercise for you. The memoize() decorator does not work with functions that accept more than one argument. How can we verify that? After verifying it, try nding a way of fixing this issue.

如果你对装饰器了解的还不够多,我留给你最后一个练习。memoize()运用于函数时接受超过一个参数就不会工作。我们如何验证这种情况?在得到验证之后,你可以试找出一个修正这个问题的办法。

总结

This chapter covered the Decorator pattern and its relation to the Python programming language. We use the Decorator pattern as a convenient way of extending the behavior of an object without using inheritance. Python extends the Decorator concept even more, by allowing us to extend the behavior of any callable (function, method, or class) without using inheritance or composition. We can use the built-in decorator feature of Python.

本章内容覆盖了装饰器模式以及它与Python编程语言的关系。我们使用装饰器模式来作为扩展一个对象的行为的很方便的方式,而不使用继承。Python更深入的扩展了装饰器,通过允许我们扩展任意可调用对象的行为(函数,方法,或者类),而不使用继承或者合成。我们使用Python内建装饰器的功能。

We have seen a few examples of objects that are decorated in reality, like guns and cameras. From a software point of view, both Django and Grok use decorators for achieving different goals, such as controlling HTTP compression and caching.

我们已经见过了真实的几个被装饰对象的例子,比如,枪和照相机。从软件的角度来看,Django和Grok都适用装饰器来实现不同的目的,比如控制HTTP压缩和缓存。

The Decorator pattern is a great solution for implementing cross-cutting concerns, because they are generic and do not fit well into the OOP paradigm. We mentioned many categories of cross-cutting concerns in the Use cases section. In fact, in the Implementation section a cross-cutting concern was demonstrated: memoization. We saw how decorators can help us to keep our functions clean, without sacrificing performance.

装饰器模式是实现关注切面的一个很好的解决方案,因为它们通用,而且并不很适合OOP范式。我们在使用案例小节提到了很多的关注切面的种类。实际上,在实现小节演示了一个关注切面的例子:记忆器。我们见到了装饰器如何帮助我们保证代码的整洁,而不用付出性能上的代价。

The recommended exercises in this chapter can help you understand decorators even better, so that you can use this very powerful tool for solving many common (and perhaps less common) programming problems. The next chapter covers the Facade pattern, which is a convenient way of simplifying access to a complex system.

本章的建议练习能够帮助你更好地理解装饰器,所以你可以使用这个非常强大的工具来解决很多常见的(可能更少见的)编程问题。在下一章,我们要讨论Facade模式,它是一个简化对负责系统访问的便利方法。