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

根据我的理解,有两种方法可以实现 Python 装饰器,一种是使用类的 __call__,另一种是定义和调用函数作为装饰器。这些方法的优缺点是什么?有没有更好的方法?

例子一

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()
35400 次浏览

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

例如,如果你的装饰工程师做了很多工作,那么你可以使用 class 作为装饰工程师,像这样:

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

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

然而,对引擎盖下的东西有一个很好的理解会使它变得自然吗 为每个场合挑选最好的选择。

一个装饰器(指的是函数装饰器) ,就是一个可调用的对象,它接受一个函数作为它的输入参数。Python 的设计相当有趣,它允许 除了函数之外,还可以创建其他类型的可调用对象,并且可以使用它 创建更易于维护或更短的代码。

在 Python 2.3中添加了装饰符,作为

def a(x):
...


a = my_decorator(a)

除此之外,当我们使用这种方法时,我们通常称装饰者为一些“可调用的”而不是“装饰工厂”:

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

使用 param1和 param2对“ my _ decorator”进行调用——然后返回一个将再次调用的对象,这次使用“ my _ func”作为参数。因此,在这种情况下,从技术上讲,“ decator”是“ my _ decator”返回的任何值,使其成为 “装潢工厂”。

现在,无论是装饰者还是所描述的“装饰工厂”通常都必须保持一些内部状态。在第一种情况下,它只保留对原始函数的引用(例子中称为 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

Update : 缺少“纯类”修饰符形式

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

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

二零一八年最新情况

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

基本思想是使用一个函数,但是返回一个自身的 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__中完成。此外,非函数的可调用非常罕见(实例化类是一个明显的例外) ,人们通常不会期望这样。此外,局部变量通常更容易让人们跟踪 vs。实例变量,仅仅是因为它们的作用域更有限,尽管在这种情况下,实例变量可能只在 __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所做的好事,最好把它省略掉。