Python类装饰器参数

38

我正尝试在Python中向我的类装饰器传递可选参数。 以下是我目前的代码:

class Cache(object):
    def __init__(self, function, max_hits=10, timeout=5):
        self.function = function
        self.max_hits = max_hits
        self.timeout = timeout
        self.cache = {}

    def __call__(self, *args):
        # Here the code returning the correct thing.


@Cache
def double(x):
    return x * 2

@Cache(max_hits=100, timeout=50)
def double(x):
    return x * 2

第二个带参数的装饰器(在我的 __init__ 函数中设置了 max_hits = 10,timeout = 5 )无法工作,我收到了异常 TypeError:__ init __()至少需要2个参数(给出3个)。我尝试了许多解决方案并阅读了相关文章,但我仍然无法使其正常工作。

有任何想法能够解决这个问题吗?谢谢!

9个回答

30

@Cache(max_hits=100, timeout=50) 调用了 __init__(max_hits=100, timeout=50),因此你没有满足 function 参数。

你可以通过一个包装方法来实现你的装饰器,该方法检测是否存在函数。如果找到函数,则返回 Cache 对象。否则,返回将用作装饰器的包装函数。

class _Cache(object):
    def __init__(self, function, max_hits=10, timeout=5):
        self.function = function
        self.max_hits = max_hits
        self.timeout = timeout
        self.cache = {}

    def __call__(self, *args):
        # Here the code returning the correct thing.

# wrap _Cache to allow for deferred calling
def Cache(function=None, max_hits=10, timeout=5):
    if function:
        return _Cache(function)
    else:
        def wrapper(function):
            return _Cache(function, max_hits, timeout)

        return wrapper

@Cache
def double(x):
    return x * 2

@Cache(max_hits=100, timeout=50)
def double(x):
    return x * 2

8
如果开发者使用位置参数而非关键字参数调用Cache(例如@Cache(100,50)),则function将被赋值为100,max_hits为50。直到调用函数时才会引发错误。这可能被视为令人惊讶的行为,因为大多数人都期望统一的位置和关键字语义。 - unutbu
1
如果我在一个对象实例方法上使用@Cache装饰器,则_Cache的__call__方法不会接收到被装饰对象的self引用。在这种情况下无法工作。 - MichaelMoser
哇,这在常规函数上运行。 - alexzander
为了解决@unutbu提到的问题,我们可以按照以下方式定义装饰器: def Cache(function=None, *, max_hits=10, timeout=5): - undefined

29
@Cache
def double(...): 
   ...

等同于

def double(...):
   ...
double=Cache(double)

@Cache(max_hits=100, timeout=50)
def double(...):
   ...

等同于

def double(...):
    ...
double = Cache(max_hits=100, timeout=50)(double)

Cache(max_hits=100, timeout=50)(double)的语义与Cache(double)非常不同。

试图让Cache同时处理这两种用例是不明智的。

您可以使用装饰器工厂,该工厂可以接受可选的max_hitstimeout参数,并返回一个装饰器:

class Cache(object):
    def __init__(self, function, max_hits=10, timeout=5):
        self.function = function
        self.max_hits = max_hits
        self.timeout = timeout
        self.cache = {}

    def __call__(self, *args):
        # Here the code returning the correct thing.

def cache_hits(max_hits=10, timeout=5):
    def _cache(function):
        return Cache(function,max_hits,timeout)
    return _cache

@cache_hits()
def double(x):
    return x * 2

@cache_hits(max_hits=100, timeout=50)
def double(x):
    return x * 2

顺便说一下,如果Cache类除了__init____call__方法没有其他方法,那么你可能可以将所有代码移动到_cache函数内部,并完全省略Cache类。


1
不明智或者说...如果开发人员不小心使用@cache而不是cache(),在调用生成的函数时会产生奇怪的错误。另一种实现实际上可以同时作为cache和cache()。 - lunixbochs
2
@lunixbochs:混淆 cache_hits(以前的 cache)和 cache_hits() 的开发人员很可能会将任何函数对象与函数调用混淆,或者将生成器错误地认为是迭代器。即使是经验不算浅的 Python 程序员也应该习惯注意到这些差异。 - unutbu

11

我更喜欢将包装器包含在类的__call__方法中:

更新: 该方法已在Python 3.6中进行了测试,因此我不确定更高或更早版本的情况。

class Cache:
    def __init__(self, max_hits=10, timeout=5):
        # Remove function from here and add it to the __call__
        self.max_hits = max_hits
        self.timeout = timeout
        self.cache = {}

    def __call__(self, function):
        def wrapper(*args):
            value = function(*args)
            # saving to cache codes
            return value
        return wrapper

@Cache()
def double(x):
    return x * 2

@Cache(max_hits=100, timeout=50)
def double(x):
    return x * 2

你尝试过在装饰器之后调用函数吗?我认为这种方法行不通。 - ahmadkarimi12
1
@AK12 你是试过了还是只是认为它不会起作用?因为我正在使用这种方法,而且它运行良好。 - Ghasem
@AK12 你试过将整个答案复制粘贴到一个新项目中,并运行例如 double(5) 吗?你的Python版本是什么? - Ghasem
1
@AK12 如果你已经复制了整个类,问题仍然存在,那么我怀疑问题可能是由于我们使用不同的Python版本引起的。 - Ghasem
1
仅供参考:这个类是一个装饰器工厂,而__call__函数定义了装饰器本身。在我看来,这比被接受的答案更加简洁。你还可以在wrapper函数前面添加@functools.wraps(function),以进一步改善你的答案。 - M.Winkens
显示剩余3条评论

4

我从这个问题中学到了很多,感谢大家。答案难道不是只要在第一个@Cache上放置空括号吗?这样你就可以将function参数移到__call__中。

class Cache(object):
    def __init__(self, max_hits=10, timeout=5):
        self.max_hits = max_hits
        self.timeout = timeout
        self.cache = {}

    def __call__(self, function, *args):
        # Here the code returning the correct thing.

@Cache()
def double(x):
    return x * 2

@Cache(max_hits=100, timeout=50)
def double(x):
    return x * 2

虽然我认为这种方法更简单、更简洁:

def cache(max_hits=10, timeout=5):
    def caching_decorator(fn):
        def decorated_fn(*args ,**kwargs):
            # Here the code returning the correct thing.
        return decorated_fn
    return decorator

如果在使用装饰器时忘记了括号,不幸的是,直到运行时你仍然不会收到错误提示,因为外部装饰器参数将被传递给您要装饰的函数。然后,在运行时,内部装饰器会报错:
``` TypeError: caching_decorator() takes exactly 1 argument (0 given). ```
但是,如果您知道您的装饰器参数永远不会是可调用对象,那么您可以捕获这种情况:
def cache(max_hits=10, timeout=5):
    assert not callable(max_hits), "@cache passed a callable - did you forget to parenthesize?"
    def caching_decorator(fn):
        def decorated_fn(*args ,**kwargs):
            # Here the code returning the correct thing.
        return decorated_fn
    return decorator

如果你现在尝试执行以下操作:
@cache
def some_method()
    pass

声明时出现 AssertionError 错误。

顺便提一下,我在寻找装饰类而不是被装饰的类的装饰器时,偶然遇到了这篇文章。如果有其他人也在寻找相似的内容,这个问题 可能会有用。


3

你可以使用classmethod作为工厂方法,这应该处理所有用例(带括号或不带括号)。

import functools
class Cache():
    def __init__(self, function):
        functools.update_wrapper(self, function)
        self.function = function
        self.max_hits = self.__class__.max_hits
        self.timeout = self.__class__.timeout
        self.cache = {}

    def __call__(self, *args):
        # Here the code returning the correct thing.
    
    @classmethod
    def Cache_dec(cls, _func = None, *, max_hits=10, timeout=5):
        cls.max_hits = max_hits
        cls.timeout = timeout
        if _func is not None: #when decorator is passed parenthesis
            return cls(_func)
        else:
            return cls    #when decorator is passed without parenthesis
       

@Cache.Cache_dec
def double(x):
    return x * 2

@Cache.Cache_dec()
def double(x):
    return x * 2

@Cache.Cache_dec(timeout=50)
def double(x):
    return x * 2

@Cache.Cache_dec(max_hits=100)
def double(x):
    return x * 2

@Cache.Cache_dec(max_hits=100, timeout=50)
def double(x):
    return x * 2

但是,对于装饰器的每个应用程序,您最终会得到相同的类实例。这是一个问题,因为每个被装饰的函数都看到相同的参数集。 - MichaelMoser

1
我为此编写了一个助手装饰器:

from functools import update_wrapper

class ClassWrapper:
    def __init__(self, cls):
        self.cls = cls
    
    def __call__(self, *args, **kwargs):
        class ClassWrapperInner:
            def __init__(self, cls, *args, **kwargs):
                # This combines previous information to get ready to recieve the actual function in the __call__ method.
                self._cls = cls
                self.args = args
                self.kwargs = kwargs
            
            def __call__(self, func, *args, **kw):
                # Basically "return self._cls(func, *self.args, **self.kwargs)", but with an adjustment to update the info of the new class & verify correct arguments
                assert len(args) == 0 and len(kw) == 0 and callable(func), f"{self._cls.__name__} got invalid arguments. Did you forget to parenthesize?"
                obj = self._cls(func, *self.args, **self.kwargs)
                update_wrapper(obj, func)
                return obj
            
        return ClassWrapperInner(self.cls, *args, **kwargs)

在执行上下文中,这段奇怪的代码更有意义:

double = ClassWrapper(Cache)(max_hits=100, timeout=50)(double)

ClassWrapper.__init__ 存储它将要包装的类(Cache)。

ClassWrapper.__call__ 将它的参数(max_hits=100, timeout=50)传递给 ClassWrapperInner.__init__,后者会为下一次调用存储它们。

ClassWrapper.__call__ 将之前所有的参数和 (func) 组合在一起,并将它们传递给你的类 Cache 的一个实例,然后将其作为新的 double 返回。它还使用 functools 库更新了你类的参数 __name__ 和 __doc__。这有点像比 2D 列表展平更复杂的版本,其中是函数参数而不是列表。

通过使用这个类来装饰它,你原始的函数将按预期工作,只是在所有情况下需要在其周围加上括号。

@ClassWrapper
class Cache(object):
    def __init__(self, function, max_hits=10, timeout=5):
        self.function = function
        self.max_hits = max_hits
        self.timeout = timeout
        self.cache = {}

    def __call__(self, *args):
        ... # Here the code returning the correct thing.

@Cache()
def double(x):
    return x * 2

@Cache(max_hits=100, timeout=50)
def double(x):
    return x * 2

你可以尝试编辑ClassWrapperInner.__call__,以便不需要使用括号调用函数,但这种方法是hacky的,并且并不是很合理;这就像尝试为类的每个方法添加逻辑,以便在没有self参数的情况下正确地调用它们。
编辑: 撰写本答案后,我意识到有一种更好的方法可以制作装饰器:
def class_wrapper(cls):
    def decorator1(*args, **kwargs):
        def decorator2(func):
            return cls(func, *args, **kwargs)
        return decorator2
    return decorator1

使用functools函数来更新名称和内容:

def class_wrapper(cls):
    def decorator1(*args, **kwargs):
        @wraps(cls)
        def decorator2(func):
            obj = cls(func, *args, **kwargs)
            update_wrapper(obj, func)
            return obj
        return decorator2
    return decorator1

0

定义一个接受可选参数的装饰器:

from functools import wraps, partial             
def _cache(func=None, *, instance=None):         
    if func is None:                             
        return partial(_cache, instance=instance)
    @wraps(func)                                 
    def wrapper(*ar, **kw):                      
        print(instance)                          
        return func(*ar, **kw)                   
    return wrapper         

instance对象传递给装饰器的__call__方法,或者使用在每个__call__方法中实例化的其他辅助类。这样,您可以在不使用括号的情况下使用装饰器,并且可以带有参数,甚至可以在代理缓存类中定义__getattr__以应用一些参数。
class Cache:                                   
    def __call__(self, *ar, **kw):             
        return _cache(*ar, instance=self, **kw)
                                               
cache = Cache()                                
                                               
@cache                                         
def f(): pass                                  
f() # prints <__main__.Cache object at 0x7f5c1bde4880>

                                       

                  

0
class myclass2:
 def __init__(self,arg):
  self.arg=arg
  print("call to init")
 def __call__(self,func):
  print("call to __call__ is made")
  self.function=func
  def myfunction(x,y,z):
   return x+y+z+self.function(x,y,z)
  self.newfunction=myfunction
  return self.newfunction
 @classmethod
 def prints(cls,arg):
  cls.prints_arg=arg
  print("call to prints is made")
  return cls(arg)


@myclass2.prints("x")
def myfunction1(x,y,z):
 return x+y+z
print(myfunction1(1,2,3))

remember it goes like this:
first call return object get second argument
usually if applicable it goes like argument,function,old function arguments

3
您的回答可以通过提供更多支持性信息来改善。请进行[编辑]以添加进一步细节,如引用或文档,以便他人确认您的答案是否正确。您可以在帮助中心找到有关撰写良好答案的更多信息。 - Community

0
你也可以使用元类来实现类装饰器。 当使用关键字参数调用装饰器时,元类的__call__方法将包装原始装饰器。
class CacheMeta(type):
    def __call__(cls, *args, **kwargs):
        factory = super().__call__

        def wrap(function):
            return factory(function, **kwargs)

        return wrap if kwargs and not args else wrap(*args)


class Cache(metaclass=CacheMeta):
    def __init__(self, function, max_hits=10, timeout=5):
        self.function = function
        self.max_hits = max_hits
        self.timeout = timeout
        self.cache = {}

    def __call__(self, *args):
        # Here the code returning the correct thing.


@Cache
def double(x):
    return x * 2

@Cache(max_hits=100, timeout=50)
def double(x):
    return x * 2

目前您的回答不够清晰明了。请编辑并添加更多细节信息,以帮助他人理解此回答如何解决问题。您可以在帮助中心了解更多编写良好回答的相关信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接