Python装饰器的常见用途有哪些?

369

虽然我认为自己是一个相当能够熟练使用Python编程的人,但是有一点是我从未能够理解的,那就是装饰器。

我知道它们是什么(表面上),我已经阅读了教程、示例、Stack Overflow上的问题,我也理解语法,可以编写自己的代码,偶尔会使用@classmethod和@staticmethod,但是我从来没有想到要在自己的Python代码中使用装饰器来解决问题。我从未遇到过这样的问题,即:“嗯...这看起来像是一个需要用装饰器来解决的工作!”

因此,我想知道大家是否可以提供一些使用装饰器解决问题的实例,希望我能有一个“啊哈!”的时刻并真正理解他们。


7
装饰器对于记忆化非常有用——即缓存函数计算结果较慢的情况。装饰器可以返回一个函数,该函数检查输入参数,如果已经存在,则返回缓存的结果。 - Peter
2
请注意,Python自带一个装饰器functools.lru_cache,它做的正是Peter所说的,自从2011年2月发布的Python 3.2版本以来一直可用。 - Ignatius
Python装饰器库的内容应该能够为您提供它们的其他用途的良好理解。 - martineau
13个回答

141

我主要使用装饰器来进行计时

def time_dec(func):

  def wrapper(*arg):
      t = time.clock()
      res = func(*arg)
      print func.func_name, time.clock()-t
      return res

  return wrapper


@time_dec
def myFunction(n):
    ...

17
在Unix下,time.clock()测量的是CPU时间。如果你想测量挂钟时间,可以考虑使用time.time()代替。 - Jabba
26
很好的例子!不过我不知道它是做什么的。能否解释一下你在做什么,以及装饰器如何解决这个问题呢? - MeLight
9
好的,它测量了 myFunction 运行所需的时间... - RSabet
@time_dec 是语法糖,等同于:myFunction = time_dec(myFunction)。其余部分是标准的Python代码。 - DMeneses

103

我已经使用它们进行同步。

import functools

def synchronized(lock):
    """ Synchronization decorator """
    def wrap(f):
        @functools.wraps(f)
        def newFunction(*args, **kw):
            lock.acquire()
            try:
                return f(*args, **kw)
            finally:
                lock.release()
        return newFunction
    return wrap

如评论中所指出的那样,自 Python 2.5 以来,您可以将 with 语句与 threading.Lock(或自版本2.6起的multiprocessing.Lock)对象结合使用,以简化装饰器的实现,只需:

import functools

def synchronized(lock):
    """ Synchronization decorator """
    def wrap(f):
        @functools.wraps(f)
        def newFunction(*args, **kw):
            with lock:
                return f(*args, **kw)
        return newFunction
    return wrap

不管怎样,你可以像这样使用它:

import threading
lock = threading.Lock()

@synchronized(lock)
def do_something():
  # etc

@synchronzied(lock)
def do_something_else():
  # etc

基本上,它只是在函数调用的两侧放置了lock.acquire() / lock.release()


23
可能是有道理的,但装饰器本质上很令人困惑,特别是对于第一年的新手来说,他们会在您之后尝试修改您的代码。通过简单实现避免这种情况:只需将 do_something() 的代码放在 'with lock:' 块内,每个人都可以清楚地看到您的目的。人们过度使用装饰器以显示自己聪明(其中许多人确实如此),但当代码面对普通人时,就变得混乱不堪了。 - Kevin J. Rice
22
将代码限制在“一年级菜鸟”可以更好地理解是可怕的做法。装饰器语法更容易阅读,大大解耦了代码。 - TaylerJones
21
@TaylerJones,当我写代码时,可读性是我最高的优先事项。每次修改代码之前,代码通常被阅读7次以上。难以理解的代码(对于新手或在时间紧迫的情况下工作的专家)是技术债务,每当有人访问源代码树时都必须付出代价。 - Kevin J. Rice
1
@TaylerJones 程序员最重要的任务之一就是提供清晰明了的代码。 - JDOaktown
1
@JDOaktown 程序员的一个重要任务是能够理解他们所使用语言的简单概念。 - DMeneses
@TaylerJones,此外,未经修饰的代码要容易调试得多,因此找到错误也更容易。 - Péter Szilvási

81

我使用装饰器来对通过某些远程方法调用传递给Python方法的参数进行类型检查。因此,我不必一遍又一遍地重复相同的参数计数、异常抛出等麻烦步骤。

例如,不必像下面这样:

def myMethod(ID, name):
    if not (myIsType(ID, 'uint') and myIsType(name, 'utf8string')):
        raise BlaBlaException() ...

我只是声明:

@accepts(uint, utf8string)
def myMethod(ID, name):
    ...

accepts() 为我完成了所有的工作。


17
对于任何有兴趣的人,PEP 318中已经有了@accepts的实现。 - martineau
2
我认为有一个打字错误..第一个方法应该是accepts..你把两个方法都声明为"myMethod"了。 - DevC
1
@DevC 不,这似乎不是打字错误。因为这显然不是“accepts(..)”的实现,而在这里,“accepts(..)”执行了在“myMethod(..)”开头将要执行的两行代码 - 这是唯一符合条件的解释。 - Evgeni Sergeev
1
抱歉打扰了,我只是想指出检查传递参数的类型并在不符合条件时引发TypeError被认为是一种不好的做法,因为它不会接受例如int类型,如果仅检查浮点数类型,并且通常代码本身应该适应不同类型的值以实现最大的灵活性。 - Gustavo6046
2
在Python中进行类型检查的推荐方式是使用内置的isinstance()函数,就像装饰器的PEP 318 实现中所做的那样。由于它的classinfo参数可以是一个或多个类型,因此使用它也可以缓解@Gustavo6046(有效的)反对意见。Python还有一个Number抽象基类,因此可以进行非常通用的测试,例如isinstance(42, numbers.Number) - martineau
显示剩余2条评论

52

Cherrypy使用@cherrypy.expose来区分哪些函数是公共的,哪些是隐藏函数。那是我的第一次接触,并从那时开始逐渐习惯了它。 - Marc Maxmeister

33

对于nosetests,您可以编写一个装饰器,为单元测试函数或方法提供多组参数:

@parameters(
   (2, 4, 6),
   (5, 6, 11),
)
def test_add(a, b, expected):
    assert a + b == expected

24

Twisted库使用装饰器和生成器结合的方式,给人一种异步函数是同步的幻觉。例如:

@inlineCallbacks
def asyncf():
    doStuff()
    yield someAsynchronousCall()
    doStuff()
    yield someAsynchronousCall()
    doStuff()

使用这种方法,原本需要分割成许多小回调函数的代码可以自然地编写为单个块,这使得代码更易于理解和维护。


19

当然,一个明显的用途是用于日志记录:

import functools

def log(logger, level='info'):
    def log_decorator(fn):
        @functools.wraps(fn)
        def wrapper(*a, **kwa):
            getattr(logger, level)(fn.__name__)
            return fn(*a, **kwa)
        return wrapper
    return log_decorator

# later that day ...
@log(logging.getLogger('main'), level='warning')
def potentially_dangerous_function(times):
    for _ in xrange(times): rockets.get_rocket(NUCLEAR=True).fire()

12

我主要使用它们进行调试(将一个函数包装起来,以打印其参数和结果),以及验证(例如检查参数的正确类型,或在Web应用程序中检查用户是否具有足够的权限来调用特定方法)。


6

最近我在开发一款社交网络应用程序时使用了它们。对于社区/群组,我需要授权成员创建新的讨论并回复消息。因此,我编写了一个装饰器@membership_required,并将其放置在视图中所需的位置。


6
我正在使用以下修饰符使函数线程安全。它使代码更易读。它几乎与John Fouhy提出的相似,但不同之处在于它适用于单个函数且无需显式创建锁对象。
def threadsafe_function(fn):
    """decorator making sure that the decorated function is thread safe"""
    lock = threading.Lock()
    def new(*args, **kwargs):
        lock.acquire()
        try:
            r = fn(*args, **kwargs)
        except Exception as e:
            raise e
        finally:
            lock.release()
        return r
    return new

class X:
    var = 0

    @threadsafe_function     
    def inc_var(self):
        X.var += 1    
        return X.var

1
这是否意味着每个被装饰的函数都有自己的锁? - grieve
1
@grieve 是的,每次使用(调用)装饰器时,它都会为被装饰的函数/方法创建一个新的锁对象。 - martineau
5
那真的很危险。方法 inc_var() 是“线程安全”的,因为一次只能有一个人调用它。尽管如此,由于该方法操作成员变量“var”,而其他方法可能也会操作成员变量“var”,这些访问不是线程安全的,因为锁没有共享。以这种方式做会给类X的用户一种虚假的安全感。 - Bob Van Zant
除非使用单一锁,否则它不是线程安全的。 - Chandu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接