网络编程
位置:首页>> 网络编程>> Python编程>> 详解Python中的装饰器、闭包和functools的教程

详解Python中的装饰器、闭包和functools的教程

作者:sahandsaba  发布时间:2023-12-30 13:43:03 

标签:Python,闭包,装饰器

装饰器(Decorators)

装饰器是这样一种设计模式:如果一个类希望添加其他类的一些功能,而不希望通过继承或是直接修改源代码实现,那么可以使用装饰器模式。简单来说Python中的装饰器就是指某些函数或其他可调用对象,以函数或类作为可选输入参数,然后返回函数或类的形式。通过这个在Python2.6版本中被新加入的特性可以用来实现装饰器设计模式。

顺便提一句,在继续阅读之前,如果你对Python中的闭包(Closure)概念不清楚,请查看本文结尾后的附录,如果没有闭包的相关概念,很难恰当的理解Python中的装饰器。

在Python中,装饰器被用于用@语法糖修辞的函数或类。现在让我们用一个简单的装饰器例子来演示如何做一个函数调用日志记录器。在这个例子中,装饰器将时间格式作为输入参数,在调用被这个装饰器装饰的函数时打印出函数调用的时间。这个装饰器当你需要手动比较两个不同算法或实现的效率时很有用。
 


def logged(time_format):
 def decorator(func):
  def decorated_func(*args, **kwargs):
    print "- Running '%s' on %s " % (
                    func.__name__,
                    time.strftime(time_format)
              )
    start_time = time.time()
    result = func(*args, **kwargs)
    end_time = time.time()
    print "- Finished '%s', execution time = %0.3fs " % (
                    func.__name__,
                    end_time - start_time
              )

return result
  decorated_func.__name__ = func.__name__
  return decorated_func
return decorator

来看一个例子,在这里add1和add2函数被logged修饰,下面给出了一个输出示例。请注意在这里时间格式参数是存储在被返回的装饰器函数中(decorated_func)。这就是为什么理解闭包对于理解装饰器来说很重要的原因。同样也请注意返回函数的名字是如何被替换为原函数名的,以防万一如果它还要被使用到,这是为了防止混淆。Python默认可不会这么做。


@logged("%b %d %Y - %H:%M:%S")
def add1(x, y):
 time.sleep(1)
 return x + y

@logged("%b %d %Y - %H:%M:%S")
def add2(x, y):
 time.sleep(2)
 return x + y

print add1(1, 2)
print add2(1, 2)

# Output:
- Running 'add1' on Jul 24 2013 - 13:40:47
- Finished 'add1', execution time = 1.001s
3
- Running 'add2' on Jul 24 2013 - 13:40:48
- Finished 'add2', execution time = 2.001s
3

如果你足够细心,你可能会注意到我们对于返回函数的名字__name__有着特别的处理,但对其他的注入__doc__或是__module__则没有如此。所以如果,在这个例子中add函数有一个doc字符串的话,它就会被丢弃。那么该如何处理呢?我们当然可以像处理__name__那样对待所有的字段,不过如果在每个装饰器内都这么做的话未免太繁冗了。这就是为何functools模块提供了一个名为wraps的装饰器的原因,那正是为了处理这种情况。可能在理解装饰器的过程中会被迷惑,不过当你把装饰器看成是一个接收函数名作为输入参数并且返回一个函数,这样就很好理解了。我们将在下个例子中使用wraps装饰器而不是手动去处理__name__或其他属性。

下个例子会有点复杂,我们的任务是将一个函数调用的返回结果缓存一段时间,输入参数决定缓存时间。传递给函数的输入参数必须是可哈希的对象,因为我们使用包含调用输入参数的tuple作为第一个参数,第二个参数则为一个frozenset对象,它包含了关键词项kwargs,并且作为cache key。每个函数都会有一个唯一的cache字典存储在函数的闭包内。

【译注】set和frozenset为Python的两种内建集合,其中前者为可变对象(mutable),其元素可以使用add()或remove()进行变更,而后者为不可变对象(imutable)并且是可哈希的(hashable),在建立之后元素不可变,他可以作为字典的key或是另一个集合的元素。


import time
from functools import wraps

def cached(timeout, logged=False):
 """Decorator to cache the result of a function call.
 Cache expires after timeout seconds.
 """
 def decorator(func):
   if logged:
     print "-- Initializing cache for", func.__name__
   cache = {}

@wraps(func)
   def decorated_function(*args, **kwargs):
     if logged:
       print "-- Called function", func.__name__
     key = (args, frozenset(kwargs.items()))
     result = None
     if key in cache:
       if logged:
         print "-- Cache hit for", func.__name__, key

(cache_hit, expiry) = cache[key]
       if time.time() - expiry < timeout:
         result = cache_hit
       elif logged:
         print "-- Cache expired for", func.__name__, key
     elif logged:
       print "-- Cache miss for", func.__name__, key

# No cache hit, or expired
     if result is None:
       result = func(*args, **kwargs)

cache[key] = (result, time.time())
     return result

return decorated_function

return decorator

来看看它的用法。我们使用装饰器装饰一个很基本的斐波拉契数生成器。这个cache装饰器将对代码使用备忘录模式(Memoize Pattern)。请注意fib函数的闭包是如何存放cache字典、一个指向原fib函数的引用、logged参数的值以及timeout参数的最后值的。dump_closure将在文末定义。

>>> @cached(10, True)
... def fib(n):
...   """Returns the n'th Fibonacci number."""
...   if n == 0 or n == 1:
...     return 1
...   return fib(n - 1) + fib(n - 2)
...
-- Initializing cache for fib
>>> dump_closure(fib)
1. Dumping function closure for fib:
-- cell 0 = {}
-- cell 1 =
-- cell 2 = True
-- cell 3 = 10
>>>
>>> print "Testing - F(4) = %d" % fib(4)
-- Called function fib
-- Cache miss for fib ((4,), frozenset([]))
-- Called function fib
-- Cache miss for fib ((3,), frozenset([]))
-- Called function fib
-- Cache miss for fib ((2,), frozenset([]))
-- Called function fib
-- Cache miss for fib ((1,), frozenset([]))
-- Called function fib
-- Cache miss for fib ((0,), frozenset([]))
-- Called function fib
-- Cache hit for fib ((1,), frozenset([]))
-- Called function fib
-- Cache hit for fib ((2,), frozenset([]))
Testing - F(4) = 5
Class Decorators

在之前的小节中,我们看了一些函数装饰器和一些使用的小技巧,接下来我们来看看类装饰器。类装饰器将一个class作为输入参数(Python中的一种类类型对象),并且返回一个修改过的class。

第一个例子是一个简单的数学问题。当给定一个有序集合P,我们定义Pd为P的反序集合P(x,y) <-> Pd(x,y),也就是说两个有序集合的元素顺序互为相反的,这在Python中该如何实现?假定一个类定义了__lt__以及__le__或其他方法来实现有序。那么我们可以通过写一个类装饰器来替换这些方法。
 


def make_dual(relation):
 @wraps(relation, ['__name__', '__doc__'])
 def dual(x, y):
   return relation(y, x)
 return dual

def dual_ordering(cls):
 """Class decorator that reverses all the orderings"""
 for func in ['__lt__', '__gt__', '__ge__', '__le__']:
   if hasattr(cls, func):
     setattr(cls, func, make_dual(getattr(cls, func)))
 return cls

下面是将这个装饰器用以str类型的例子,创建一个名为rstr的新类,使用反字典序(opposite lexicographic)为其顺序。


@dual_ordering
class rstr(str):
 pass

x = rstr("1")
y = rstr("2")

print x < y
print x <= y
print x > y
print x >= y

# Output:
False
False
True
True

来看一个更复杂的例子。假定我们希望前面所说的logged装饰器能够被用于某个类的所有方法。一个方案是在每个类方法上都加上装饰器。另一个方案是写一个类装饰器自动完成这些工作。在动手之前,我将把前例中的logged装饰器拿出来做一些小改进。首先,它使用functools提供的wraps装饰器完成固定__name__的工作。第二,一个_logged_decorator属性被引入(设置为True的布尔型变量),用来指示这个方法是否已经被装饰器装饰过,因为这个类可能会被继承而子类也许会继续使用装饰器。最后,name_prefix参数被加入用来设置打印的日志信息。
 


def logged(time_format, name_prefix=""):
 def decorator(func):
   if hasattr(func, '_logged_decorator') and func._logged_decorator:
     return func

@wraps(func)
   def decorated_func(*args, **kwargs):
     start_time = time.time()
     print "- Running '%s' on %s " % (
                     name_prefix + func.__name__,
                     time.strftime(time_format)
                )
     result = func(*args, **kwargs)
     end_time = time.time()
     print "- Finished '%s', execution time = %0.3fs " % (
                     name_prefix + func.__name__,
                     end_time - start_time
                )

return result
   decorated_func._logged_decorator = True
   return decorated_func
 return decorator

好的,让我们开始写类装饰器:
 


def log_method_calls(time_format):
 def decorator(cls):
   for o in dir(cls):
     if o.startswith('__'):
       continue
     a = getattr(cls, o)
     if hasattr(a, '__call__'):
       decorated_a = logged(time_format, cls.__name__ + ".")(a)
       setattr(cls, o, decorated_a)
   return cls
 return decorator

下面是使用方法,注意被继承的或被重写的方法是如何处理的。


@log_method_calls("%b %d %Y - %H:%M:%S")
class A(object):
 def test1(self):
   print "test1"

@log_method_calls("%b %d %Y - %H:%M:%S")
class B(A):
 def test1(self):
   super(B, self).test1()
   print "child test1"

def test2(self):
   print "test2"

b = B()
b.test1()
b.test2()

# Output:
- Running 'B.test1' on Jul 24 2013 - 14:15:03
- Running 'A.test1' on Jul 24 2013 - 14:15:03
test1
- Finished 'A.test1', execution time = 0.000s
child test1
- Finished 'B.test1', execution time = 1.001s
- Running 'B.test2' on Jul 24 2013 - 14:15:04
test2
- Finished 'B.test2', execution time = 2.001s

我们第一个类装饰器的例子是类的反序方法。一个相似的装饰器,可以说是相当有用的,实现__lt__、__le__、__gt__、__ge__和__eq__中的一个,能够实现类的全排序么?这也就是functools.total_ordering装饰器所做的工作。详情请见参考文档。
Flask中的一些例子

让我们来看看Flask中用到的一些有趣的装饰器。

假定你希望让某些函数在特定的调用时刻输出警告信息,例如仅仅在debug模式下。而你又不希望每个函数都加入控制的代码,那么你就能够使用装饰器来实现。以下就是Flask的app.py中定义的装饰器的工作。
 


def setupmethod(f):
 """Wraps a method so that it performs a check in debug mode if the
 first request was already handled.
 """
 def wrapper_func(self, *args, **kwargs):
   if self.debug and self._got_first_request:
     raise AssertionError('A setup function was called after the '
       'first request was handled. This usually indicates a bug '
       'in the application where a module was not imported '
       'and decorators or other functionality was called too late.\n'
       'To fix this make sure to import all your view modules, '
       'database models and everything related at a central place '
       'before the application starts serving requests.')
   return f(self, *args, **kwargs)
 return update_wrapper(wrapper_func, f)

来看一个更有趣的例子,这个例子是Flask的route装饰器,在Flask类中定义。注意到装饰器可以是类中的一个方法,将self作为第一个参数。完整的代码在app.py中。请注意装饰器简单的将被装饰过的函数注册成为一个URL句柄,这是通过调用add_url_rule函数来实现的。


def route(self, rule, **options):
"""A decorator that is used to register a view function for a
given URL rule. This does the same thing as :meth:`add_url_rule`
but is intended for decorator usage::

@app.route('/')
  def index():
    return 'Hello World'

For more information refer to :ref:`url-route-registrations`.

:param rule: the URL rule as string
:param endpoint: the endpoint for the registered URL rule. Flask
        itself assumes the name of the view function as
        endpoint
:param options: the options to be forwarded to the underlying
        :class:`~werkzeug.routing.Rule` object. A change
        to Werkzeug is handling of method options. methods
        is a list of methods this rule should be limited
        to (`GET`, `POST` etc.). By default a rule
        just listens for `GET` (and implicitly `HEAD`).
        Starting with Flask 0.6, `OPTIONS` is implicitly
        added and handled by the standard request handling.
"""
def decorator(f):
  endpoint = options.pop('endpoint', None)
  self.add_url_rule(rule, endpoint, f, **options)
  return f
return decorator

扩展阅读

1. official Python Wiki

2. metaprogramming in Python 3
附录:闭包

一个函数闭包是一个函数和一个引用集合的组合,这个引用集合指向这个函数被定义的作用域的变量。后者通常指向一个引用环境(referencing environment),这使得函数能够在它被定义的区域之外执行。在Python中,这个引用环境被存储在一个cell的tuple中。你能够通过func_closure或Python 3中的__closure__属性访问它。要铭记的一点是引用及是引用,而不是对象的深度拷贝。当然了,对于不可变对象而言,这并不是问题,然而对可变对象(list)这点就必须注意,随后会有一个例子说明。请注意函数在定义的地方也有__globals__字段来存储全局引用环境。

来看一个简单的例子:
 


>>> def return_func_that_prints_s(s):
...   def f():
...       print s
...   return f
...
>>> g = return_func_that_prints_s("Hello")
>>> h = return_func_that_prints_s("World")
>>> g()
Hello
>>> h()
World
>>> g is h
False
>>> h.__closure__
(,)
>>> print [str(c.cell_contents) for c in g.__closure__]
['Hello']
>>> print [str(c.cell_contents) for c in h.__closure__]
['World']

一个稍复杂的例子。确保明白为什么会这么执行。
 


>>> def return_func_that_prints_list(z):
...   def f():
...       print z
...   return f
...
>>> z = [1, 2]
>>> g = return_func_that_prints_list(z)
>>> g()
[1, 2]
>>> z.append(3)
>>> g()
[1, 2, 3]
>>> z = [1]
>>> g()
[1, 2, 3]

【译者】:z.append(3)时,g()内部的引用和z仍然指向一个变量,而z=[1]之后,两者就不再指向一个变量了。

最后,来看看代码中使用到的dump_closure方法的定义。
 


def dump_closure(f):
 if hasattr(f, "__closure__") and f.__closure__ is not None:
   print "- Dumping function closure for %s:" % f.__name__
   for i, c in enumerate(f.__closure__):
     print "-- cell %d = %s" % (i, c.cell_contents)
 else:
   print " - %s has no closure!" % f.__name__
0
投稿

猜你喜欢

  •     可控制的滚动新闻不同于自动的滚动条,它是通过按钮控制移动的,当你把鼠标放在按钮上时,新闻内容就会向上或
  • 在用 Javascript 验证表单(form)中的单选框(radio)是否选中时,很多新手都会遇到问题,原因是 radio 和普通的文本框
  • 说到运维报警,我觉得都可以写个长篇历史来详细解释了报警的前世来生,比如最早报警都是用邮件,但邮件实时性不高,比如下班回家总不能人一直盯着邮箱
  • FCKeditor的样式设置涉及到了两个文件,一个是你定义好的样式表文件.css,另一个是告诉fck样式表如何使用的xml文件,两个文件确一
  • 在本教程中,你会学到如何把HTML的列表项(li元素)转换成下图的“便签墙”。该效果分5步实现。内核为webkit的Safari和Chrom
  • 经常由于各种压缩格式的不一样用到文件的解压缩时就需要下载不同的解压缩工具去处理不同的文件,以至于桌面上的压缩工具就有三四种,于是使用pyth
  • 1.1 闭包1、闭包概念1. 在一个外函数中定义了一个内函数,内函数里运用了外函数的临时变量,并且外函数的返回值是内函数的引用,这样就构成了
  • 在制作网页以及编程的时候,适当的进行注释,不仅使自己的思路清晰,极大地减轻了维护的难度,而且方便项目组其他人了解你的代码,方便对代码的理解以
  •  首先选择操作系统。由于ASP属于MS(Microsoft)的东西,所以我们要选择MS的操作系统,Windows 98以上就可以(
  • 前言前几天逛github发现了一个有趣的并发库-conc,其目标是:更难出现goroutine泄漏处理panic更友好并发代码可读性高从简介
  • 如果你完全不懂,那么期望1-2周看完一遍拉倒....不用看的太仔细,后面再看到不懂的时候回头去看这些东西好了1. 前言和准备工作 这里不会介
  • PDO::inTransactionPDO::inTransaction — 检查是否在一个事务内(PHP 5 >= 5.3.3, B
  • 为什么我把自己机子上的数据库备份文件往另一台机子上还原不成功?可能是你在Restore的对话框中选项不正确。Restore 有三个选项,分别
  • -- 任意的测试表 代码如下:CREATE TABLE test_delete( name varchar(10), value INT )
  • eval()函数可以将字符串型的list、tuple、dict等等转换为原有的数据类型即使用eval可以实现从元组,列表,字典型的字符串到元
  • 听到一些人说现在做产品设计很没有成就感。没有什么创造力,除了抄袭模仿(称之为竞争分析)、千篇一律(又称规范标准)还有复杂的流程、粗制滥造的表
  • 本文实例讲述了Python实现求两个数组交集的方法。分享给大家供大家参考,具体如下:一、题目给定两个数组,编写一个函数来计算它们的交集。例1
  • MySQL有6种日志,监控数据库系统的时候必须知道select日志slow select日志变更日志二进制变更日志(binlog) 
  • 在本节描述的示例代码,提供真实的例子来示范在 FileSystemObject 对象模式中可用的许多功能。该代码显示了如何一起使用对象模式的
  • 下一步是将新创建的应用程序与项目相关联。为此,您需要编辑 myproj 文件夹中的 settings.py 文件,将字符串“myproj.m
手机版 网络编程 asp之家 www.aspxhome.com