【问题标题】:Add a decorator to a class add a decorator to class' methods by decorating class向类添加装饰器 通过装饰类向类的方法添加装饰器
【发布时间】:2021-12-31 10:27:48
【问题描述】:

我正在尝试创建一个可以在类上定义并装饰其中定义的所有内容的装饰器。首先让我展示一下我已经根据其他 SO 答案获得的设置:

import inspect


# https://stackoverflow.com/a/18421294/577669
def log(func):
    def wrapped(*args, **kwargs):
        try:
            print("Entering: [%s]" % func)
            try:
                # https://stackoverflow.com/questions/19227724/check-if-a-function-uses-classmethod
                if inspect.ismethod(func) and func.__self__:  # class method
                    return func(*args[1:], **kwargs)
                if inspect.isdatadescriptor(func):
                    return func.fget(args[0])
                return func(*args, **kwargs)
            except Exception as e:
                print('Exception in %s : (%s) %s' % (func, e.__class__.__name__, e))
        finally:
            print("Exiting: [%s]" % func)
    return wrapped


class trace(object):
    def __call__(self, cls):  # instance, owner):
        for name, m in inspect.getmembers(cls, lambda x: inspect.ismethod(x) or inspect.isfunction(x)):
            setattr(cls, name, log(m))
        for name, m in inspect.getmembers(cls, lambda x: inspect.isdatadescriptor(x)):
            setattr(cls, name, property(log(m)))
        return cls


@trace()
class Test:
    def __init__(self, arg):
        self.arg = arg

    @staticmethod
    def static_method(arg):
        return f'static: {arg}'

    @classmethod
    def class_method(cls, arg):
        return f'class: {arg}'

    @property
    def myprop(self):
        return 'myprop'

    def normal(self, arg):
        return f'normal: {arg}'


if __name__ == '__main__':
    test = Test(1)
    print(test.arg)
    print(test.static_method(2))
    print(test.class_method(3))
    print(test.myprop)
    print(test.normal(4))

当从类中移除 @trace 装饰器时,输出如下:

123
static
class
myprop
normal

当添加 @trace 装饰器时,我得到了这个:

Entering: [<function Test.__init__ at 0x00000170FA9ED558>]
Exiting: [<function Test.__init__ at 0x00000170FA9ED558>]
1
Entering: [<function Test.static_method at 0x00000170FB308288>]
Exception in <function Test.static_method at 0x00000170FB308288> : (TypeError) static_method() takes 1 positional argument but 2 were given
Exiting: [<function Test.static_method at 0x00000170FB308288>]
None
Entering: [<bound method Test.class_method of <class '__main__.Test'>>]
Exiting: [<bound method Test.class_method of <class '__main__.Test'>>]
class: 3
Entering: [<property object at 0x00000170FB303E08>]
Exiting: [<property object at 0x00000170FB303E08>]
myprop
Entering: [<function Test.normal at 0x00000170FB308438>]
Exiting: [<function Test.normal at 0x00000170FB308438>]
normal: 4

此示例的结论:init、normal、class 和 prop 方法都正确检测。

但是,静态方法不是。

我对这个 sn-p 的问题是:

  1. 可以像我在日志中那样检查某些用例吗?还是有更好的方法?
  2. 如何查看某个东西是否是静态方法才能不传入任何内容(因为现在传入的是 Test-instance)?

谢谢!

【问题讨论】:

  • 测试我的答案,我注意到您的输出与我在您的代码中所期望的不符。您是否更改了一些测试数据?

标签: python decorator


【解决方案1】:

我查看了inspect的源代码,发现它可以在classify_class_attrs中找到静态方法,所以我修改了你的代码以使用该函数。

我还分离了日志,这样我就可以使用不同的包装函数来处理不同的规则。其中一些是多余的,但这就是我最初分离静态方法的方式。我担心 classmethod 应该得到 cls 参数,也许这是一个合理的担忧,但它通过了这些简单的测试而没有成为问题。

import inspect
import types

# https://stackoverflow.com/a/18421294/577669
def log(func, *args, **kwargs):
    try:
        print("Entering: [%s]" % func)
        try:
            if callable(func):
                return func(*args, **kwargs)
        except Exception as e:
            print('Exception in %s : (%s) %s' % (func, e.__class__.__name__, e))
            raise e
    finally:
        print("Exiting: [%s]" % func)

def log_function(func):
    def wrapped(*args, **kwargs):
        return log(func, *args, **kwargs)
    return wrapped
    
def log_staticmethod(func):
    def wrapped(*args, **kwargs):
        return log(func, *args[1:], **kwargs)
    return wrapped
    
def log_method(func):
    def wrapped(*args, **kwargs):
        instance = args[0]
        return log(func, *args, **kwargs)
    return wrapped
    
def log_classmethod(func):
    def wrapped(*args, **kwargs):
        return log(func, *args[1:], **kwargs)
    return wrapped

def log_datadescriptor(name, getter):
    def wrapped(*args, **kwargs):
        instance = args[0]
        return log(getter.fget, instance)
    return wrapped
    
class trace(object):
    def __call__(self, cls):  # instance, owner):
        for result in inspect.classify_class_attrs(cls):
            if result.defining_class == cls:
                func = getattr(cls, result.name, None)
                if result.kind == 'method':
                    setattr(cls, result.name, log_method(func))
                if result.kind == 'class method':
                    setattr(cls, result.name, log_classmethod(func))
                if result.kind == 'static method':
                    setattr(cls, result.name, log_staticmethod(func))
        for name, getter in inspect.getmembers(cls, inspect.isdatadescriptor):
            setattr(cls, name, property(log_datadescriptor(name, getter)))
        return cls


@trace()
class Test:
    def __init__(self, arg):
        self.value = arg

    @staticmethod
    def static_method(arg):
        return f'static: {arg}'

    @classmethod
    def class_method(cls, arg):
        return f'class Test, argument: {arg}'

    @property
    def myprop(self):
        return f'myprop on instance {self.value}'

    def normal(self, arg):
        return f'normal: {arg} on instance {self.value}'


if __name__ == '__main__':
    test = Test(123)
    print(test.value)
    print(test.static_method(2))
    print(test.class_method(3))
    print(test.myprop)
    print(test.normal(4))

输入:[init at 0x000002338FDCCA60>]
退出:[init at 0x000002338FDCCA60>]
123
输入:[]
退出:[]
静态:2
输入:[ma​​in.Test'>>]
退出:[ma​​in.Test'>>]
类测试,参数:3
输入:[]
退出:[]
实例 123 上的 myprop
输入:[]
退出:[]
正常:实例 123 上为 4

某些文本不完全匹配,因为我们都对跟踪类中的输出进行了一些微不足道的更改。

【讨论】:

  • 我注意到这也跟踪 weakref 也许它应该跳过任何 dunder 名称?
  • 另外,我不确定如何处理来自classify_class_attrs结果的属性getter,所以我只是复制了你已经在工作的代码来处理这些。
  • 感谢“classify_class_attrs”方法。我遇到的一个问题是在装饰基类和派生类时。你有办法解决这个问题吗?
  • 如果您对静态方法的答案感到满意,我会调查一下。
  • 非常感谢您的提示。我想我以一种合理的干净方式解决了这个问题。我发布了我的解决方案,但不确定该怎么做。您对classify_class_attrs 的提示确实使我找到了解决方案。请告诉我如何进行。
【解决方案2】:

我的最终解决方案:

import inspect
from typing import Type

from decorator import decorator


@decorator
def log(func, *args, **kwargs):
    try:
        print("Entering: [%s]" % func)
        return func(*args, **kwargs)
    finally:
        print("Exiting: [%s]" % func)


def _apply_logger_to_class(cls):
    for attr in inspect.classify_class_attrs(cls):
        if attr.defining_class is not cls:
            continue
        if attr.kind == 'data':
            continue

        if isinstance(attr.object, (classmethod, staticmethod)):
            setattr(cls, attr.name, attr.object.__class__(log(attr.object.__func__)))
        elif isinstance(attr.object, property):
            setattr(cls, attr.name, property(log(attr.object.fget)))
        else:
            setattr(cls, attr.name, log(attr.object))

    return cls


def trace(func=None):
    if isinstance(func, type):
        return _apply_logger_to_class(func)  # logger is the class

    return log(func)


@trace
def normal_function_call(arg):
    return f'normal_function_call: {arg}'


@trace
class Test:
    def __init__(self, arg):
        self.arg = arg
        print(f'{self.__class__.__name__}.__init__: {arg}')

    @staticmethod
    def static_method(arg):
        return f'Test.static: {1}'

    @classmethod
    def class_method(cls, arg):
        print(f'{cls.__name__}.class: {arg}')

    @property
    def myprop(self):
        print(f'{self.__class__.__name__}.myprop.getter')
        return 1

    @myprop.setter
    def myprop(self, item):
        print(f'{self.__class__.__name__}.myprop.setter')

    @myprop.deleter
    def myprop(self):
        print(f'{self.__class__.__name__}.myprop.deleter')

    def normal(self, arg):
        print(f'{self.__class__.__name__}.normal: {arg}')


@trace
class TestDerived(Test):
    @staticmethod
    def static_method(arg):
        print(f'TestDerived.class: {arg}')


if __name__ == '__main__':
    print(normal_function_call(0))

    def do_test(test_class: Type[Test]):
        print('-'*20, test_class.__name__)

        test = test_class(1)
        test.static_method(2)
        test.__class__.static_method(2.5)
        test.class_method(3)
        test.__class__.class_method(3.5)
        test.myprop
        test.normal(4)

        assert inspect.getfullargspec(test.normal).args == ['self', 'arg']
        assert inspect.getfullargspec(test.normal).kwonlyargs == []

    do_test(Test)
    do_test(TestDerived)

这适用于派生类,适用于我想要的所有对象,并保留签名。 (@wraps 没有)。

【讨论】:

    猜你喜欢
    • 2011-07-25
    • 1970-01-01
    • 2022-10-13
    • 1970-01-01
    • 2019-07-15
    • 1970-01-01
    • 2014-01-14
    • 2020-01-11
    • 1970-01-01
    相关资源
    最近更新 更多