繁体   English   中英

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

[英]Python decorator best practice, using a class vs a function

据我所知,有两种方法可以做一个 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.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, ...):

我主要同意 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)

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

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

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

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

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__ 可以解决此限制,但它非常笨拙。 最好简单地接受需要这些括号。

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM