【问题标题】:Python decorator best practice, using a class vs a functionPython 装饰器最佳实践,使用类与函数
【发布时间】:2012-05-04 20:37:59
【问题描述】:

据我了解,有两种方法可以做 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()

【问题讨论】:

标签: python python-2.7 decorator syntactic-sugar


【解决方案1】:

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

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

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()

【讨论】:

  • 是的,但我仍然想知道这两种方式的优缺点以及何时使用什么。好吧,我猜类装饰器可以更高级一点?有什么缺点吗?
  • 好的,装饰器是一个函数或类,它在输入时接受函数或类。而现在你可以想到在哪些情况下使用函数比较方便,或者什么时候要使用类。
  • 这个答案在概念上是错误的:这不是一个“类装饰器”——一个“类装饰器”装饰一个类,它与它的实现是一个类还是一个函数无关。跨度>
【解决方案2】:

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

但是,对底层内容的深入了解会让事情变得自然 让人们为每个场合选择最佳选择。

装饰器(谈论函数装饰器)只是一个可调用对象,它将函数作为其输入参数。 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, ...):

【讨论】:

  • 很棒的答案,谢谢。为了避免“self.mode”,您还可以在 call 中返回一个函数。例如def __call__(self, func): def wrapper(*args, **kwargs): return func(*args,**kwargs) return wrapper
  • 所有使用class 可调用对象的方法都有同样的问题,它们不能使用wraps。您是否考虑过添加一个返回的__str__(self),例如"<function %s at 0x%x>" % (self.func.__name__, hash(self))?也可以将 __name____doc__ 从 wrapee 复制到实例(例如,MyDecoratorStage2Decorator),但这在 IPython 中并没有真正的帮助。
  • 抱歉 - 这篇文章的目的不是“伪装一个装饰函数,使其与原始函数无法区分”。当然,任何人都可以创建一个 __repr____str__ 几乎没有用)方法,其中包含有关包装内容的信息。你还是有道理的 - functools.wraps 不仅仅是简单地复制包装函数 __name__,并且没有现成的等效于可以与基于类的方法一起使用的功能。
  • 不错@jsbueno。谢谢你。但是问题:当使用装饰工厂的功能时,您可以查看参数并根据它们更改签名(@wraps)。如果您为此目的使用一个类,您将如何做到这一点?你看到问题了吗?你可以@wraps__call__,但只能在类的范围内,还没有看到参数值。
  • 非常好的答案,我还要说,不是使用 2019 年编辑中提供的 /,而是可以使用 *,这恰恰相反:它指定参数仅是关键字.
【解决方案3】:

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

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

Tangent:无论是类还是函数,都应该使用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)

【讨论】:

  • 我所说的“混合方法”只是让__call__ 方法本身就像一个装饰器——将要装饰的函数作为参数。你能举一个不会这样做的类装饰器的例子吗? (为此,__call__ 必须在另一个对象中提供对象,可能属于另一个类,与第一个紧密耦合 - 或 - 保持状态以“知道”它是否已经绑定到函数. - 我觉得这两件事都比让__call__ 包装装饰函数本身更令人困惑。
【解决方案4】:

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

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

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

这里有两个等效且非常简单的装饰器,用于两种风格(函数式和面向对象):

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 所做的好事情,最好将其排除在外。

【讨论】:

  • 在最后一个示例中,您可以通过在 return self.wrapper 之前调用 update_wrapper(self.wrapper.__func__, f) 来完成 functools.wraps 所做的事情。
  • 注意:您的解决方案(如CacheClassWithOptions所示)不适用于装饰实例方法。
猜你喜欢
  • 2014-03-02
  • 1970-01-01
  • 2010-10-04
  • 2011-10-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多