【问题标题】:How to decorate a class?如何装饰班级?
【发布时间】:2010-10-15 11:32:48
【问题描述】:

在 Python 2.5 中,有没有办法创建装饰类的装饰器?具体来说,我想使用装饰器将成员添加到类并更改构造函数以获取该成员的值。

正在寻找类似以下的内容(在 'class Foo:' 上有语法错误:

def getId(self): return self.__id

class addID(original_class):
    def __init__(self, id, *args, **kws):
        self.__id = id
        self.getId = getId
        original_class.__init__(self, *args, **kws)

@addID
class Foo:
    def __init__(self, value1):
        self.value1 = value1

if __name__ == '__main__':
    foo1 = Foo(5,1)
    print foo1.value1, foo1.getId()
    foo2 = Foo(15,2)
    print foo2.value1, foo2.getId()

我想我真正追求的是一种在 Python 中执行类似 C# 接口的方法。我想我需要转换我的范式。

【问题讨论】:

    标签: python decorator python-2.5


    【解决方案1】:

    除了类装饰器是否是您问题的正确解决方案之外:

    在 Python 2.6 及更高版本中,有带有 @-syntax 的类装饰器,因此您可以编写:

    @addID
    class Foo:
        pass
    

    在旧版本中,您可以采用其他方式:

    class Foo:
        pass
    
    Foo = addID(Foo)
    

    但是请注意,这与函数装饰器的工作方式相同,并且装饰器应该返回新的(或修改后的原始)类,这不是您在示例中所做的。 addID 装饰器看起来像这样:

    def addID(original_class):
        orig_init = original_class.__init__
        # Make copy of original __init__, so we can call it without recursion
    
        def __init__(self, id, *args, **kws):
            self.__id = id
            self.getId = getId
            orig_init(self, *args, **kws) # Call the original __init__
    
        original_class.__init__ = __init__ # Set the class' __init__ to the new one
        return original_class
    

    然后,您可以如上所述为您的 Python 版本使用适当的语法。

    但我同意其他人的观点,如果您想覆盖 __init__,继承更适合。

    【讨论】:

    • ...即使问题特别提到了 Python 2.5 :-)
    • 对不起,linesep 混乱,但代码示例在 cmets 中并不是严格意义上的...: def addID(original_class): original_class.__orig__init__ = original_class.__init__ def init__(self, * args, **kws): print "decorator" self.id = 9 original_class.__orig__init__(self, *args, **kws) original_class.__init = init return original_class @addID class Foo: def __init__(self): print "Foo" a = Foo() print a.id
    • @Steven,@Gerald 是对的,如果你能更新你的答案,阅读他的代码会容易得多;)
    • @Day:谢谢提醒,我之前没有注意到cmets。但是:我也得到了 Geralds 代码超出的最大递归深度。我会尝试制作一个工作版本;-)
    • @Day 和@Gerald:抱歉,问题不在于 Gerald 的代码,因为评论中的代码混乱,我搞砸了 ;-)
    【解决方案2】:

    我会支持您可能希望考虑使用子类而不是您概述的方法的概念。但是,不知道您的具体情况,YMMV :-)

    你想到的是一个元类。元类中的__new__ 函数被传递给类的完整建议定义,然后它可以在创建类之前对其进行重写。那时,您可以将构造函数替换为新的构造函数。

    例子:

    def substitute_init(self, id, *args, **kwargs):
        pass
    
    class FooMeta(type):
    
        def __new__(cls, name, bases, attrs):
            attrs['__init__'] = substitute_init
            return super(FooMeta, cls).__new__(cls, name, bases, attrs)
    
    class Foo(object):
    
        __metaclass__ = FooMeta
    
        def __init__(self, value1):
            pass
    

    替换构造函数可能有点戏剧性,但该语言确实提供了对这种深度自省和动态修改的支持。

    【讨论】:

    • 谢谢,这就是我要找的。一个类,可以修改任意数量的其他类,使它们都有一个特定的成员。我不让这些类从公共 ID 类继承的原因是我希望拥有这些类的非 ID 版本以及 ID 版本。
    • 元类曾经是在 Python2.5 或更早版本中执行此类操作的首选方式,但现在您可以经常使用类装饰器(参见 Steven 的回答),这要简单得多。
    【解决方案3】:

    没有人解释过您可以动态定义类。所以你可以有一个装饰器来定义(并返回)一个子类:

    def addId(cls):
    
        class AddId(cls):
    
            def __init__(self, id, *args, **kargs):
                super(AddId, self).__init__(*args, **kargs)
                self.__id = id
    
            def getId(self):
                return self.__id
    
        return AddId
    

    可以在 Python 2 中使用(来自 Blckknght 的评论解释了为什么您应该在 2.6+ 中继续这样做),如下所示:

    class Foo:
        pass
    
    FooId = addId(Foo)
    

    在 Python 3 中像这样(但要小心在你的类中使用 super()):

    @addId
    class Foo:
        pass
    

    所以你可以让你的蛋糕和吃掉 - 继承和装饰器!

    【讨论】:

    • 装饰器中的子类化在 Python 2.6+ 中是危险的,因为它会破坏原始类中的 super 调用。如果Foo 有一个名为foo 的方法调用super(Foo, self).foo(),它将无限递归,因为名称Foo 绑定到装饰器返回的子类,而不是原始类(不能通过任何名称访问) . Python 3 的无参数 super() 避免了这个问题(我假设通过相同的编译器魔法让它完全可以工作)。您还可以通过以不同的名称手动装饰类来解决此问题(就像您在 Python 2.5 示例中所做的那样)。
    • 嗯。谢谢,我不知道(我使用 python 3)。将添加评论。
    【解决方案4】:

    这不是一个好的做法,因此没有机制可以做到这一点。完成你想要的事情的正确方法是继承。

    看看class documentation。

    一个小例子:

    class Employee(object):
    
        def __init__(self, age, sex, siblings=0):
            self.age = age
            self.sex = sex    
            self.siblings = siblings
    
        def born_on(self):    
            today = datetime.date.today()
    
            return today - datetime.timedelta(days=self.age*365)
    
    
    class Boss(Employee):    
        def __init__(self, age, sex, siblings=0, bonus=0):
            self.bonus = bonus
            Employee.__init__(self, age, sex, siblings)
    

    这样,Boss 拥有Employee 拥有的一切,还有他自己的__init__ 方法和自己的成员。

    【讨论】:

    • 我想我想要的是让 Boss 不知道它包含的类。也就是说,可能有几十个不同的类我想应用 Boss 功能。我还剩下这十几个类从 Boss 继承吗?
    • @Robert Gowland:这就是 Python 具有多重继承的原因。是的,您应该从各个父类继承各个方面。
    • @S.Lott:一般来说,多重继承是个坏主意,即使继承的层次过多也是不好的。我会建议你远离多重继承。
    • mpeterson:python 中的多重继承是否比这种方法更糟糕? python的多重继承有什么问题?
    • @Arafangion:多重继承通常被认为是麻烦的警告信号。它导致复杂的层次结构和难以遵循的关系。如果您的问题域非常适合多继承,(可以分层建模吗?)它是许多人的自然选择。这适用于所有允许多重继承的语言。
    【解决方案5】:

    我同意继承更适合所提出的问题。

    我发现这个问题真的很方便,虽然在装饰课上,谢谢大家。

    这是基于其他答案的另外几个示例,包括继承如何影响 Python 2.7 中的事物(以及 @wraps,它维护原始函数的文档字符串等):

    def dec(klass):
        old_foo = klass.foo
        @wraps(klass.foo)
        def decorated_foo(self, *args ,**kwargs):
            print('@decorator pre %s' % msg)
            old_foo(self, *args, **kwargs)
            print('@decorator post %s' % msg)
        klass.foo = decorated_foo
        return klass
    
    @dec  # No parentheses
    class Foo...
    

    通常你想给你的装饰器添加参数:

    from functools import wraps
    
    def dec(msg='default'):
        def decorator(klass):
            old_foo = klass.foo
            @wraps(klass.foo)
            def decorated_foo(self, *args ,**kwargs):
                print('@decorator pre %s' % msg)
                old_foo(self, *args, **kwargs)
                print('@decorator post %s' % msg)
            klass.foo = decorated_foo
            return klass
        return decorator
    
    @dec('foo decorator')  # You must add parentheses now, even if they're empty
    class Foo(object):
        def foo(self, *args, **kwargs):
            print('foo.foo()')
    
    @dec('subfoo decorator')
    class SubFoo(Foo):
        def foo(self, *args, **kwargs):
            print('subfoo.foo() pre')
            super(SubFoo, self).foo(*args, **kwargs)
            print('subfoo.foo() post')
    
    @dec('subsubfoo decorator')
    class SubSubFoo(SubFoo):
        def foo(self, *args, **kwargs):
            print('subsubfoo.foo() pre')
            super(SubSubFoo, self).foo(*args, **kwargs)
            print('subsubfoo.foo() post')
    
    SubSubFoo().foo()
    

    输出:

    @decorator pre subsubfoo decorator
    subsubfoo.foo() pre
    @decorator pre subfoo decorator
    subfoo.foo() pre
    @decorator pre foo decorator
    foo.foo()
    @decorator post foo decorator
    subfoo.foo() post
    @decorator post subfoo decorator
    subsubfoo.foo() post
    @decorator post subsubfoo decorator
    

    我使用了函数装饰器,因为我发现它们更简洁。这是一个装饰类的类:

    class Dec(object):
    
        def __init__(self, msg):
            self.msg = msg
    
        def __call__(self, klass):
            old_foo = klass.foo
            msg = self.msg
            def decorated_foo(self, *args, **kwargs):
                print('@decorator pre %s' % msg)
                old_foo(self, *args, **kwargs)
                print('@decorator post %s' % msg)
            klass.foo = decorated_foo
            return klass
    

    一个更健壮的版本,可以检查这些括号,并在装饰类中不存在方法时工作:

    from inspect import isclass
    
    def decorate_if(condition, decorator):
        return decorator if condition else lambda x: x
    
    def dec(msg):
        # Only use if your decorator's first parameter is never a class
        assert not isclass(msg)
    
        def decorator(klass):
            old_foo = getattr(klass, 'foo', None)
    
            @decorate_if(old_foo, wraps(klass.foo))
            def decorated_foo(self, *args ,**kwargs):
                print('@decorator pre %s' % msg)
                if callable(old_foo):
                    old_foo(self, *args, **kwargs)
                print('@decorator post %s' % msg)
    
            klass.foo = decorated_foo
            return klass
    
        return decorator
    

    assert 检查装饰器是否在没有括号的情况下未被使用。如果有,则被装饰的类被传​​递给装饰器的msg 参数,从而引发AssertionError。

    @decorate_if 仅在 condition 计算结果为 True 时应用 decorator。

    使用getattr、callable 测试和@decorate_if 是为了在被装饰的类上不存在foo() 方法时不会破坏装饰器。

    【讨论】:

      【解决方案6】:

      这里实际上有一个很好的类装饰器实现:

      https://github.com/agiliq/Django-parsley/blob/master/parsley/decorators.py

      我实际上认为这是一个非常有趣的实现。因为它继承了它所装饰的类,所以它在 isinstance 检查之类的事情上的行为与这个类完全一样。

      它还有一个额外的好处:自定义 django 表单中的 __init__ 语句对 self.fields 进行修改或添加的情况并不少见,因此最好在 之后 对 self.fields 进行更改> __init__ 的所有成员都参加了相关课程。

      非常聪明。

      但是,在您的类中,您实际上希望装饰来改变构造函数,我认为这对于类装饰器来说不是一个好的用例。

      【讨论】:

        【解决方案7】:

        这是一个回答返回类参数问题的示例。此外,它仍然尊重继承链,即只返回类本身的参数。函数get_params 是作为一个简单示例添加的,但由于检查模块,可以添加其他功能。

        import inspect 
        
        class Parent:
            @classmethod
            def get_params(my_class):
                return list(inspect.signature(my_class).parameters.keys())
        
        class OtherParent:
            def __init__(self, a, b, c):
                pass
        
        class Child(Parent, OtherParent):
            def __init__(self, x, y, z):
                pass
        
        print(Child.get_params())
        >>['x', 'y', 'z']
        
        

        【讨论】:

          【解决方案8】:

          Django 有method_decorator,它是一个装饰器,可以将任何装饰器变成方法装饰器,您可以在django.utils.decorators 中看到它是如何实现的:

          https://github.com/django/django/blob/50cf183d219face91822c75fa0a15fe2fe3cb32d/django/utils/decorators.py#L53

          https://docs.djangoproject.com/en/3.0/topics/class-based-views/intro/#decorating-the-class

          【讨论】:

            猜你喜欢
            • 2021-03-17
            • 2015-08-13
            • 2016-01-13
            • 2021-05-19
            • 2014-06-07
            • 2020-05-11
            • 2020-10-20
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多