70

据我了解,有两种方法可以做 Python 装饰器,要么使用__call__类的 ,要么定义和调用函数作为装饰器。这些方法的优点/缺点是什么?有一种首选方法吗?

示例 1

class dec1(object):
    def __init__(self, f):
        self.f = f
    def __call__(self):
        print "Decorating", self.f.__name__
        self.f()

@dec1
def func1():
    print "inside func1()"

func1()

# Decorating func1
# inside func1()

示例 2

def dec2(f):
    def new_f():
        print "Decorating", f.__name__
        f()
    return new_f

@dec2
def func2():
    print "inside func2()"

func2()

# Decorating func2
# inside func2()
4

4 回答 4

88

说每种方法是否有“优点”是相当主观的。

但是,对引擎盖下的内容有一个很好的了解将使人们很自然地为每个场合选择最佳选择。

装饰器(谈论函数装饰器)只是一个以函数作为输入参数的可调用对象。Python 有其相当有趣的设计,它允许人们创建除函数之外的其他类型的可调用对象——并且有时可以将其用于创建更可维护或更短的代码。

在 Python 2.3 中添加了装饰器作为“语法快捷方式”

def a(x):
   ...

a = my_decorator(a)

除此之外,我们通常将装饰器称为一些“可调用对象”,而不是“装饰器工厂”——当我们使用这种类型时:

@my_decorator(param1, param2)
def my_func(...):
   ...

使用 param1 和 param2 对“my_decorator”进行调用 - 然后返回一个将再次调用的对象,这次将“my_func”作为参数。因此,在这种情况下,从技术上讲,“装饰器”是“my_decorator”返回的任何内容,使其成为“装饰器工厂”。

现在,描述的装饰器或“装饰器工厂”通常必须保持一些内部状态。在第一种情况下,它唯一保留的是对原始函数的引用(f在您的示例中调用的变量)。“装饰工厂”可能想要注册额外的状态变量(上例中的“param1”和“param2”)。

在作为函数编写的装饰器的情况下,这种额外状态保存在封闭函数内的变量中,并由实际的包装函数作为“非本地”变量访问。如果编写了一个适当的类,它们可以作为实例变量保存在装饰器函数中(这将被视为“可调用对象”,而不是“函数”) - 并且对它们的访问更加明确且更具可读性。

因此,在大多数情况下,您是否喜欢一种方法或另一种方法取决于可读性:对于简短的简单装饰器,函数式方法通常比作为类编写的方法更具可读性 - 而有时更复杂的方法 - 尤其是一种“装饰工厂”将充分利用 Python 编码前的“平面优于嵌套”建议。

考虑:

def my_dec_factory(param1, param2):
   ...
   ...
   def real_decorator(func):
       ...
       def wraper_func(*args, **kwargs):
           ...
           #use param1
           result = func(*args, **kwargs)
           #use param2
           return result
       return wraper_func
   return real_decorator

针对这种“混合”解决方案:

class MyDecorator(object):
    """Decorator example mixing class and function definitions."""
    def __init__(self, func, param1, param2):
        self.func = func
        self.param1, self.param2 = param1, param2

    def __call__(self, *args, **kwargs):
        ...
        #use self.param1
        result = self.func(*args, **kwargs)
        #use self.param2
        return result

def my_dec_factory(param1, param2):
    def decorator(func):
         return MyDecorator(func, param1, param2)
    return decorator

更新:缺少“纯类”装饰器形式

现在,请注意“混合”方法采用“两全其美”的方法,试图保持最短且更易读的代码。一个完全用类定义的“装饰器工厂”要么需要两个类,要么需要一个“模式”属性来知道它是被调用来注册装饰函数还是实际调用最终函数:

class MyDecorator(object):
   """Decorator example defined entirely as class."""
   def __init__(self, p1, p2):
        self.p1 = p1
        ...
        self.mode = "decorating"

   def __call__(self, *args, **kw):
        if self.mode == "decorating":
             self.func = args[0]
             self.mode = "calling"
             return self
         # code to run prior to function call
         result = self.func(*args, **kw)
         # code to run after function call
         return result

@MyDecorator(p1, ...)
def myfunc():
    ...

最后是一个定义了两个类的纯“白领”装饰器——也许让事情更加分离,但将冗余增加到不能说它更易于维护的程度:

class Stage2Decorator(object):
    def __init__(self, func, p1, p2, ...):
         self.func = func
         self.p1 = p1
         ...
    def __call__(self, *args, **kw):
         # code to run prior to function call
         ...
         result = self.func(*args, **kw)
         # code to run after function call
         ...
         return result

class Stage1Decorator(object):
   """Decorator example defined as two classes.
   
   No "hacks" on the object model, most bureacratic.
   """
   def __init__(self, p1, p2):
        self.p1 = p1
        ...
        self.mode = "decorating"

   def __call__(self, func):
       return Stage2Decorator(func, self.p1, self.p2, ...)


@Stage1Decorator(p1, p2, ...)
def myfunc():
    ...

2018 年更新

几年前我写了上面的文字。我最近想出了一个我更喜欢的模式,因为它创建了“更扁平”的代码。

基本思想是使用函数,但partial如果在用作装饰器之前使用参数调用它,则返回自身的对象:

from functools import wraps, partial

def decorator(func=None, parameter1=None, parameter2=None, ...):

   if not func:
        # The only drawback is that for functions there is no thing
        # like "self" - we have to rely on the decorator 
        # function name on the module namespace
        return partial(decorator, parameter1=parameter1, parameter2=parameter2)
   @wraps(func)
   def wrapper(*args, **kwargs):
        # Decorator code-  parameter1, etc... can be used 
        # freely here
        return func(*args, **kwargs)
   return wrapper

就是这样 - 使用这种模式编写的装饰器可以立即装饰一个函数,而无需首先“调用”:

@decorator
def my_func():
    pass

或自定义参数:

@decorator(parameter1="example.com", ...):
def my_func():
    pass
        
        

2019 - 使用 Python 3.8 和仅位置参数,最后一种模式将变得更好,因为func参数可以声明为仅位置参数,并且需要命名参数;

def decorator(func=None, *, parameter1=None, parameter2=None, ...):
于 2012-04-24T15:23:22.520 回答
11

我大多同意 jsbueno:没有一种正确的方法。这取决于实际情况。但我认为 def 在大多数情况下可能会更好,因为如果你上课,大多数“真正的”工作__call__无论如何都会完成。此外,非函数的可调用对象非常少见(除了实例化类的显着例外),人们通常不希望出现这种情况。此外,与实例变量相比,局部变量通常更容易让人们跟踪,这仅仅是因为它们的范围更有限,尽管在这种情况下,实例变量可能仅用于__call____init__只需从参数中复制它们)。

不过,我不得不不同意他的混合方法。这是一个有趣的设计,但我认为它可能会让你或几个月后看到它的其他人感到困惑。

切线:无论您使用类还是函数,都应该使用functools.wraps,它本身就是用作装饰器(我们必须更深入!),如下所示:

import functools

def require_authorization(f):
    @functools.wraps(f)
    def decorated(user, *args, **kwargs):
        if not is_authorized(user):
            raise UserIsNotAuthorized
        return f(user, *args, **kwargs)
    return decorated

@require_authorization
def check_email(user, etc):
    # etc.

这使得decorated看起来像check_email例如通过改变它的func_name属性。

无论如何,这通常是我所做的,也是我看到周围其他人所做的,除非我想要一个装饰工厂。在这种情况下,我只需添加另一个级别的 def:

def require_authorization(action):
    def decorate(f):
        @functools.wraps(f):
        def decorated(user, *args, **kwargs):
            if not is_allowed_to(user, action):
                raise UserIsNotAuthorized(action, user)
            return f(user, *args, **kwargs)
        return decorated
    return decorate

顺便说一句,我也会提防过度使用装饰器,因为它们会使跟踪堆栈跟踪变得非常困难。

管理可怕的堆栈跟踪的一种方法是制定不大幅改变被装饰者行为的策略。例如

def log_call(f):
    @functools.wraps(f)
    def decorated(*args, **kwargs):
        logging.debug('call being made: %s(*%r, **%r)',
                      f.func_name, args, kwargs)
        return f(*args, **kwargs)
    return decorated

保持堆栈跟踪健全的一种更极端的方法是让装饰器返回未修改的被装饰者,如下所示:

import threading

DEPRECATED_LOCK = threading.Lock()
DEPRECATED = set()

def deprecated(f):
    with DEPRECATED_LOCK:
        DEPRECATED.add(f)
    return f

@deprecated
def old_hack():
    # etc.

如果在知道deprecated装饰器的框架内调用函数,这将很有用。例如

class MyLamerFramework(object):
    def register_handler(self, maybe_deprecated):
        if not self.allow_deprecated and is_deprecated(f):
            raise ValueError(
                'Attempted to register deprecated function %s as a handler.'
                % f.func_name)
        self._handlers.add(maybe_deprecated)
于 2014-06-04T16:41:45.553 回答
5

在这个问题最初提出近七年后,我敢于对这个问题提出不同的方法。以前的任何(非常好的!)答案中都没有描述此版本。

这里已经很好地描述了使用类和函数作为装饰器的最大区别。为了完整起见,我将再次简要介绍一下,但为了更实用,我将使用一个具体的示例。

假设您想编写一个装饰器来缓存某些缓存服务中“纯”函数的结果(那些没有副作用,因此返回值是确定性的,给定参数)。

这里有两个等效且非常简单的装饰器,在两种风格(功能和面向对象)中:

import json
import your_cache_service as cache

def cache_func(f):
    def wrapper(*args, **kwargs):
        key = json.dumps([f.__name__, args, kwargs])
        cached_value = cache.get(key)
        if cached_value is not None:
            print('cache HIT')
            return cached_value
        print('cache MISS')
        value = f(*args, **kwargs)
        cache.set(key, value)
        return value
    return wrapper

class CacheClass(object):
    def __init__(self, f):
        self.orig_func = f

    def __call__(self, *args, **kwargs):
        key = json.dumps([self.orig_func.__name__, args, kwargs])
        cached_value = cache.get(key)
        if cached_value is not None:
            print('cache HIT')
            return cached_value
        print('cache MISS')
        value = self.orig_func(*args, **kwargs)
        cache.set(key, value)
        return value

我想这很容易理解。这只是一个愚蠢的例子!为简单起见,我将跳过所有错误处理和边缘情况。无论如何,您不应该使用 StackOverflow 中的 ctrl+c/ctrl+v 代码,对吧?;)

可以注意到,两个版本本质上是相同的。面向对象的版本比函数的版本更长更冗长,因为我们必须定义方法并使用变量self,但我认为它更具可读性。这个因素对于更复杂的装饰器变得非常重要。我们马上就会看到。

上面的装饰器是这样使用的:

@cache_func
def test_one(a, b=0, c=1):
    return (a + b)*c

# Behind the scenes:
#     test_one = cache_func(test_one)

print(test_one(3, 4, 6))
print(test_one(3, 4, 6))

# Prints:
#     cache MISS
#     42
#     cache HIT
#     42

@CacheClass
def test_two(x, y=0, z=1):
    return (x + y)*z

# Behind the scenes:
#     test_two = CacheClass(test_two)

print(test_two(1, 1, 569))
print(test_two(1, 1, 569))

# Prints:
#     cache MISS
#     1138
#     cache HIT
#     1138

但是现在假设您的缓存服务支持为每个缓存条目设置 TTL。您需要在装饰时间定义它。怎么做?

传统的函数式方法是添加一个新的包装层,它返回一个配置的装饰器(在这个问题的其他答案中有更好的建议):

import json
import your_cache_service as cache

def cache_func_with_options(ttl=None):
    def configured_decorator(*args, **kwargs):
        def wrapper(*args, **kwargs):
            key = json.dumps([f.__name__, args, kwargs])
            cached_value = cache.get(key)
            if cached_value is not None:
                print('cache HIT')
                return cached_value
            print('cache MISS')
            value = f(*args, **kwargs)
            cache.set(key, value, ttl=ttl)
            return value
        return wrapper
    return configured_decorator

它是这样使用的:

from time import sleep

@cache_func_with_options(ttl=100)
def test_three(a, b=0, c=1):
    return hex((a + b)*c)

# Behind the scenes:
#     test_three = cache_func_with_options(ttl=100)(test_three)

print(test_three(8731))
print(test_three(8731))
sleep(0.2)
print(test_three(8731))

# Prints:
#     cache MISS
#     0x221b
#     cache HIT
#     0x221b
#     cache MISS
#     0x221b

这个还是可以的,但我不得不承认,即使是一位经验丰富的开发人员,有时我也会看到自己花费大量时间来理解遵循这种模式的更复杂的装饰器。这里棘手的部分是实际上不可能“取消嵌套”函数,因为内部函数需要在外部函数范围内定义的变量。

面向对象的版本可以提供帮助吗?我认为是这样,但是如果您遵循基于类的结构,它最终会得到与功能结构相同的嵌套结构,或者更糟糕的是,使用标志来保存装饰器正在执行的操作的状态(不是好的)。

因此,与其在方法中接收要装饰的函数并在__init__方法中处理包装和装饰器参数__call__(或使用多个类/函数这样做,这对我来说太复杂了),我的建议是处理装饰器方法中的参数__init__,接收方法中的函数,__call__最后在__call__.

它看起来像这样:

import json
import your_cache_service as cache

class CacheClassWithOptions(object):
    def __init__(self, ttl=None):
        self.ttl = ttl

    def __call__(self, f):
        self.orig_func = f
        return self.wrapper

    def wrapper(self, *args, **kwargs):
        key = json.dumps([self.orig_func.__name__, args, kwargs])
        cached_value = cache.get(key)
        if cached_value is not None:
            print('cache HIT')
            return cached_value
        print('cache MISS')
        value = self.orig_func(*args, **kwargs)
        cache.set(key, value, ttl=self.ttl)
        return value

用法如预期:

from time import sleep

@CacheClassWithOptions(ttl=100)
def test_four(x, y=0, z=1):
    return (x + y)*z

# Behind the scenes:
#     test_four = CacheClassWithOptions(ttl=100)(test_four)

print(test_four(21, 42, 27))
print(test_four(21, 42, 27))
sleep(0.2)
print(test_four(21, 42, 27))

# Prints:
#     cache MISS
#     1701
#     cache HIT
#     1701
#     cache MISS
#     1701

由于任何事情都是完美的,最后一种方法有两个小缺点:

  1. 不能直接使用装饰@CacheClassWithOptions@CacheClassWithOptions()即使我们不想传递任何参数,我们也必须使用括号。这是因为我们需要先创建实例,然后再尝试装饰,所以__call__方法会接收到要装饰的函数,而不是在__init__. 可以解决此限制,但它非常hacky。最好简单地接受需要这些括号。

  2. 没有明显的地方可以functools.wraps在返回的包装函数上应用装饰器,这在函数版本中是显而易见的。但是,可以通过__call__在返回之前在内部创建一个中间函数来轻松完成。它只是看起来不太好,如果您不需要这样做的好东西,最好将其排除在外functools.wraps

于 2019-03-27T11:02:45.673 回答
2

有两种不同的装饰器实现。其中一个使用类作为装饰器,另一个使用函数作为装饰器。您必须根据需要选择首选实施。

例如,如果您的装饰器做了很多工作,那么您可以使用类作为装饰器,如下所示:

import logging
import time
import pymongo
import hashlib
import random

DEBUG_MODE = True

class logger(object):

        def __new__(cls, *args, **kwargs):
                if DEBUG_MODE:
                        return object.__new__(cls, *args, **kwargs)
                else:
                        return args[0]

        def __init__(self, foo):
                self.foo = foo
                logging.basicConfig(filename='exceptions.log', format='%(levelname)s %   (asctime)s: %(message)s')
                self.log = logging.getLogger(__name__)

        def __call__(self, *args, **kwargs):
                def _log():
                        try:
                               t = time.time()
                               func_hash = self._make_hash(t)
                               col = self._make_db_connection()
                               log_record = {'func_name':self.foo.__name__, 'start_time':t, 'func_hash':func_hash}
                               col.insert(log_record)
                               res = self.foo(*args, **kwargs)
                               log_record = {'func_name':self.foo.__name__, 'exc_time':round(time.time() - t,4), 'end_time':time.time(),'func_hash':func_hash}
                               col.insert(log_record)
                               return res
                        except Exception as e:
                               self.log.error(e)
                return _log()

        def _make_db_connection(self):
                connection = pymongo.Connection()
                db = connection.logger
                collection = db.log
                return collection

        def _make_hash(self, t):
                m = hashlib.md5()
                m.update(str(t)+str(random.randrange(1,10)))
                return m.hexdigest()
于 2012-04-24T08:42:56.090 回答