今天早上我遇到了一个有趣的问题。 我有一个如下所示的基类:
# base.py
class Base(object):
@classmethod
def exists(cls, **kwargs):
# do some work
pass
还有一个看起来像这样的装饰器模块:
# caching.py
# actual caching decorator
def cached(ttl):
# complicated
def cached_model(ttl=300):
def closure(model_class):
# ...
# eventually:
exists_decorator = cached(ttl=ttl)
model_class.exists = exists_decorator(model_class.exists))
return model_class
return closure
这是我的子类模型:
@cached_model(ttl=300)
class Model(Base):
pass
事实是,当我实际调用 Model.exists 时,我收到关于参数数量错误的抱怨! 检查装饰器中的参数没有发现任何奇怪的情况 - 这些参数正是我所期望的,并且它们与方法签名匹配。 如何向已经用
classmethod
装饰的方法添加更多装饰器?
并非所有模型都被缓存,但存在()方法作为类方法存在于每个模型上,因此重新排序装饰器不是一个选项:
cached_model
可以将类方法添加到exists(),但是什么使存在() 未缓存模型上的类方法?
在Python中,当在函数体中声明一个方法时,它就像一个函数 - 一旦类被解析并存在,就通过“.”检索方法。来自实例的运算符将该函数即时转换为方法。此转换确实将第一个参数添加到方法中(如果它不是静态方法)-
在Python 3中,从类中检索方法将产生原始函数 - “未绑定方法”的形象不再存在:
>>> class A:
... def b(self):
... pass
...
>>> A.b
<function A.b at 0x7fbebfd22160>
>>> A.b is A.b
True
>>> a = A()
>>> a.b
<bound method A.b of <__main__.A object at 0x7fbebfd282f0>>
>>> a.b is a.b
False
但是在旧的 Python 2 中:
>>> class A(object):
... def b(self):
... pass
...
>>> A.b is A.b
False
因为每次检索“A”的“b”属性都会产生“方法对象 b”的不同实例
>>> A.b
<unbound method A.b>
在Python 2中,如果需要的话,可以在不进行任何转换的情况下检索原始函数“b”
>>> A.__dict__["b"]
<function b at 0xe36230>
(再次强调:这在现代 Python 中是不需要的 - 我们使用的是 3.12 版本)
对于用
@classmethod
修饰的函数,也会发生同样的情况,并且当从 A 检索值“class”时,它会被添加到参数列表中。
@classmethod
和@staticmethod
装饰器会将底层函数包装在与普通实例方法不同的描述符中。一个 classmethod 对象 - 当函数用 classmethod
包装时就会变成一个描述符对象,它有一个 '_get_' 方法,它将返回一个包装底层函数的函数 - 并添加“cls”参数位于所有其他参数之前。
@classmethod
的任何进一步装饰器都必须“知道”它实际上是在处理描述符对象,而不是函数。 -
>>> class A(object):
... @classmethod
... def b(cls):
... print b
...
>>> A.__dict__["b"]
<classmethod object at 0xd97a28>
因此,让
@classmethod
装饰器成为最后一个应用于该方法的装饰器(堆栈上的第一个装饰器)要容易得多 - 以便其他装饰器处理一个简单的函数(知道“ cls”参数将作为第一个插入)。
感谢jsbueno提供有关Python的信息。 我正在根据“装饰类的所有方法”的情况来寻找这个问题的答案。 基于寻找这个问题的答案和 jsbueno 的回应,我能够收集到以下内容:
def for_all_methods(decorator):
def decorate(cls):
for attr in dir(cls):
possible_method = getattr(cls, attr)
if not callable(possible_method):
continue
# staticmethod
if not hasattr(possible_method, "__self__"):
raw_function = cls.__dict__[attr].__func__
decorated_method = decorator(raw_function)
decorated_method = staticmethod(decorated_method)
# classmethod
elif type(possible_method.__self__) == type:
raw_function = cls.__dict__[attr].__func__
decorated_method = decorator(raw_function)
decorated_method = classmethod(decorated_method)
# instance method
elif possible_method.__self__ is None:
decorated_method = decorator(possible_method)
setattr(cls, attr, decorated_method)
return cls
return decorate
有一些冗余和一些变体,您可以使用它来减少一点。
据我所知,在某些情况下,除了将方法绑定到类之外,
class
参数。 解决方案是编辑我的班级装饰闭包:
def cached_model(ttl=300):
def closure(model_class):
# ...
# eventually:
exists_decorator = cached(ttl=ttl, cache_key=exists_cache_key)
model_class.exists = classmethod(exists_decorator(model_class.exists.im_func))
return model_class
return closure
im_func
属性似乎获取了对原始函数的引用,这使我能够使用缓存装饰器访问并装饰原始函数,然后将整个混乱包装在 classmethod
调用中。 总结,
classmethod
装饰是不可堆叠的,因为参数似乎是被注入的。只是一个功能示例,可以添加到 Scott Lobdell 的精彩答案中......
messages.py
from distutils.cmd import Command
import functools
import unittest
def for_all_methods(decorator):
def decorate(cls):
for attr in cls.__dict__:
possible_method = getattr(cls, attr)
if not callable(possible_method):
continue
# staticmethod
if not hasattr(possible_method, "__self__"):
raw_function = cls.__dict__[attr].__func__
decorated_method = decorator(raw_function)
decorated_method = staticmethod(decorated_method)
# classmethod
if type(possible_method.__self__) == type:
raw_function = cls.__dict__[attr].__func__
decorated_method = decorator(raw_function)
decorated_method = classmethod(decorated_method)
# instance method
elif possible_method.__self__ is None:
decorated_method = decorator(possible_method)
setattr(cls, attr, decorated_method)
return cls
return decorate
def add_arguments(func):
"""
The add_arguments decorator simply add the passed in arguments
(args and kwargs) the returned error message.
"""
@functools.wraps(func)
def wrapped(self, *args, **kwargs):
try:
message = func(self, *args, **kwargs)
message = ''.join([message,
"[ args:'", str(args), "'] ",
"[ kwargs:'", str(kwargs), "' ] "
])
return message
except Exception as e:
err_message = ''.join(["errorhandler.messages.MESSAGE: '",
str(func),
"(", str(args), str(kwargs), ")' ",
"FAILED FOR UNKNOWN REASON. ",
" [ ORIGINAL ERROR: ", str(e), " ] "
])
return err_message
return wrapped
@for_all_methods(add_arguments)
class MESSAGE(object):
"""
log.error(MSG.triggerPhrase(args, kwargs))
"""
@classmethod
def TEMPLATE(self, *args, **kwargs):
message = "This is a template of a pre-digested message."
return message
用法
from messages import MESSAGE
if __name__ == '__main__':
result = MESSAGE.TEMPLATE(1,2,test=3)
print result
输出
This is a template of a pre-digested message.[ args:'(1, 2)'] [ kwargs:'{'test': 3}' ]