装饰一个已经是类方法的方法?

发布于 2024-12-29 01:35:22 字数 917 浏览 0 评论 0原文

今天早上我遇到了一个有趣的问题。我有一个看起来像这样的基类:

# 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可以将类方法添加到存在(),但是那么是什么使得存在()成为未缓存模型上的类方法呢?

I had an interesting problem this morning. I had a base class that looked like this:

# base.py
class Base(object):

    @classmethod
    def exists(cls, **kwargs):
        # do some work
        pass

And a decorator module that looked like this:

# 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

Here's my subclass model:

@cached_model(ttl=300)
class Model(Base):
    pass

Thing is, when I actually call Model.exists, I get complaints about the wrong number of arguments! Inspecting the arguments in the decorator shows nothing weird going on - the arguments are exactly what I expect, and they match up with the method signature. How can I add further decorators to a method that's already decorated with classmethod?

Not all models are cached, but the exists() method is present on every model as a classmethod, so re-ordering the decorators isn't an option: cached_model can add classmethod to exists(), but then what makes exists() a classmethod on uncached models?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

如若梦似彩虹 2025-01-05 01:35:22

在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”参数将作为第一个参数插入)。

In Python, when a method is declared, in a function body, it is exactly like a function -
once the class is parsed and exists, retrieving the method through the "." operator from an instance, transforms that function - on the fly - into a method. This transform does add the first parameter to the method (if it is not an staticmethod) -

In Python 3, retrieving the method from a class will yield the original function - the figure of the "unbound method" does not exist anymore:

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

But in old Python 2:

>>> class A(object):
...    def b(self):
...        pass
... 
>>> A.b is A.b
False

Because each retrieving of the "b" attribute of "A" yields a different instance of the "method object b"

>>> A.b
<unbound method A.b>

In Python 2, the original function "b" can be retrieved without any transform if one does

>>> A.__dict__["b"]
<function b at 0xe36230>

(again: this is not needed in modern Python - we are at version 3.12)

For a function decorated with @classmethod just the same happens, and the value "class" is added to the parameter list when it is retrieved from A.

The @classmethod and @staticmethod decorators will wrap the underlying function in a different descriptor than the normal instancemethod. A classmethod object - which is what a function becomes when it is wrapped with classmethod is a descriptor object, which has a '_get_' method which will return a function wrapping the underlying function - and adding the "cls" parameter before all the other ones.

Any further decorator to a @classmethod has to "know" it is actually dealing with a descriptor object, not a function. -

>>> class A(object):
...    @classmethod
...    def b(cls):
...       print b
... 
>>> A.__dict__["b"]
<classmethod object at 0xd97a28>

So, it is a lot easier to let the @classmethod decorator to be the last one to be applied to the method (the first one on the stack) - so that the other decorators work on a simple function (knowing that the "cls" argument will be inserted as the first one).

衣神在巴黎 2025-01-05 01:35:22

感谢 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

有一些冗余和一些变化,你可以用它来减少一点。

Thanks to jsbueno for the information about Python. I was looking for an answer to this question based on the case of decorating all methods of a class. Based on looking for an answer to this question and jsbueno's reponse, I was able to gather something along the lines of:

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

There's a bit of redundancy and a few variations you could use to chop this down a bit.

丘比特射中我 2025-01-05 01:35:22

据我所知,在某些情况下,除了将方法绑定到类之外,classmethod 装饰器实际上还会在调用方法之前添加一个 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 装饰是不可堆叠的,因为参数似乎是被注入的。

The classmethod decorator actually prepends a class argument to calls to the method, in certain circumstances, as far as I can tell, in addition to binding the method to the class. The solution was editing my class decoration closure:

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

The im_func property appears to get a reference to the original function, which allows me to reach in and decorate the original function with my caching decorator, and then wrap that whole mess in a classmethod call. Summary, classmethod decorations are not stackable, because arguments seem to be injected.

猥︴琐丶欲为 2025-01-05 01:35:22

只是一个功能示例,可以添加到 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}' ] 

Just a functional example to add to Scott Lobdell's great answer...

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

usage

from messages import MESSAGE

if __name__ == '__main__':
    result = MESSAGE.TEMPLATE(1,2,test=3)
    print result

output

This is a template of a pre-digested message.[ args:'(1, 2)'] [ kwargs:'{'test': 3}' ] 
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文