Python装饰器的最佳实践,使用类vs函数

olo*_*fom 53 python decorator syntactic-sugar python-2.7

正如我所理解的那样,有两种方法可以做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()
Run Code Online (Sandbox Code Playgroud)

例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()
Run Code Online (Sandbox Code Playgroud)

jsb*_*eno 58

说每个方法是否有"优点"是相当主观的.

然而,很好地理解引擎盖下的内容会让人很自然地为每个场合选择最佳选择.

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

装饰器在Python 2.3中作为"语法快捷方式"添加回来

def a(x):
   ...

a = my_decorator(a)
Run Code Online (Sandbox Code Playgroud)

除此之外,我们通常将装饰器称为"callables",而不是"装饰工厂" - 当我们使用这种类型时:

@my_decorator(param1, param2)
def my_func(...):
   ...
Run Code Online (Sandbox Code Playgroud)

使用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
Run Code Online (Sandbox Code Playgroud)

反对这种"混合"解决方案:

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
Run Code Online (Sandbox Code Playgroud)

更新:缺少装饰的"纯类"形式

现在,请注意"混合"方法采用"两个世界中最好的",试图保持最短和更可读的代码.完全由类定义的"装饰器工厂"要么需要两个类,要么"模式"属性要知道它是否被调用来注册装饰函数或实际调用最终函数:

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():
    ...
Run Code Online (Sandbox Code Playgroud)

最后,一个纯粹的"白色colar"装饰器定义了两个类 - 可能使事物更加分离,但是将冗余增加到一点不能说它更易于维护:

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():
    ...
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

@decorator
def my_func():
    pass
Run Code Online (Sandbox Code Playgroud)

或者使用参数自定义:

@decorator(parameter1="example.com", ...):
def my_func():
    pass
Run Code Online (Sandbox Code Playgroud)


all*_*ode 9

我大多同意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.
Run Code Online (Sandbox Code Playgroud)

这使得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
Run Code Online (Sandbox Code Playgroud)

顺便说一句,我也会防止过度使用装饰器,因为它们可能使得很难跟踪堆栈跟踪.

管理可怕堆栈跟踪的一种方法是具有基本上不改变装饰物行为的策略.例如

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
Run Code Online (Sandbox Code Playgroud)

保持堆栈跟踪正确的更极端的方法是装饰器不修改返回decoratee,如下所示:

import threading

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

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

@deprecated
def old_hack():
    # etc.
Run Code Online (Sandbox Code Playgroud)

如果在了解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)
Run Code Online (Sandbox Code Playgroud)


Vic*_*der 7

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

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

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

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

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
Run Code Online (Sandbox Code Playgroud)

我想这很容易理解。这只是一个愚蠢的例子!为了简单起见,我跳过了所有错误处理和边缘情况。无论如何,你不应该从 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
Run Code Online (Sandbox Code Playgroud)

但现在假设您的缓存服务支持为每个缓存条目设置 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
Run Code Online (Sandbox Code Playgroud)

它的使用方式如下:

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
Run Code Online (Sandbox Code Playgroud)

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

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

因此,我的建议是处理装饰器,而不是在方法中接收要装饰的函数__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
Run Code Online (Sandbox Code Playgroud)

用法如预期:

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
Run Code Online (Sandbox Code Playgroud)

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

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

  2. 没有明显的地方可以functools.wraps在返回的包装函数上应用装饰器,这在函数版本中是理所当然的。不过,通过在返回之前在内部创建一个中间函数,可以轻松完成此操作__call__。它只是看起来不太好,如果您不需要这些好东西,最好将其保留functools.wraps