如何装饰一堂课?


129

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

寻找类似以下的内容(在“ 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#接口的方法。我想我应该改变我的范式。

Answers:


80

我的观点是您可能希望考虑一个子类,而不是您概述的方法。但是,不知道您的特定情况,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的回答),这要简单得多。
乔纳森·哈特利

203

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

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

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


2
...即使该问题专门提到了Python 2.5 :-)
Jarret Hardie,2009年

5
很抱歉,lineep混乱,但是代码示例在注释中并不严格...:def addID(original_class):original_class .__ orig__init__ = original_class .__ init__ def init __(self,* args,** kws):打印“ decorator” self.id = 9 original_class .__ orig__init __(self,* args,** kws)original_class .__ init = init 返回original_class @addID类Foo:def __init __(self):打印“ Foo” a = Foo()打印a.id
Gerald Senarclens de Grancy

2
@ Steven,@ Gerald是正确的,如果您可以更新答案,那么阅读他的代码会容易很多;)

3
@Day:感谢您的提醒,我之前没有注意到这些评论。但是:我也获得了Geralds代码超出的最大递归深度。我会尝试制作一个可工作的版本;-)
史蒂文(Steven)

1
@Day和@Gerald:抱歉,问题与Gerald的代码无关,由于注释中的代码错误,我搞砸了;-)
史蒂文

20

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

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

所以,你可以有你的蛋糕吃它-继承装饰!


4
在Python 2.6+中,装饰器中的子类化很危险,因为它会中断super原始类中的调用。如果Foo有一个名为的方法foosuper(Foo, self).foo()它将被无限递归,因为该名称Foo绑定到装饰器返回的子类上,而不是原始类(该类不能通过任何名称访问)。Python 3的无参数super()避免了这个问题(我想通过相同的编译器魔术使它完全可以工作)。您还可以通过用不同的名称手动装饰类来解决该问题(就像在Python 2.5示例中所做的那样)。
Blckknght

1
嗯 谢谢,我不知道(我使用python 3)。将添加评论。
安德鲁·库克

13

这不是一个好习惯,因此,没有机制可以做到这一点。完成所需内容的正确方法是继承。

查看类文档

一个小例子:

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)

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


3
我想我想让Boss知道它所包含的类。也就是说,我想将Boss功能应用到许多不同的类。我是否剩下让这十几个类从Boss继承?
罗伯特·高兰

5
@Robert Gowland:这就是Python具有多重继承的原因。是的,您应该从各个父类继承各个方面。
S.Lott

7
@ S.Lott:通常,多重继承是一个坏主意,即使过多的继承级别也是不好的。我建议您远离多重继承。
mpeterson

5
mpeterson:python中的多重继承是否比这种方法差?python的多重继承有什么问题?
Arafangion

2
@Arafangion:多重继承通常被视为麻烦的警告信号。它导致复杂的层次结构和难以遵循的关系。如果您的问题域很适合多重继承(可以对它进行分层建模吗?),这对于许多人来说是很自然的选择。这适用于所有允许多继承的语言。
Morten Jensen

6

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

我发现这个问题在装饰类上确实很方便,谢谢大家。

这是另外两个基于其他答案的示例,包括继承如何影响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仅将decoratorif condition求值应用于True

使用getattrcallable测试和@decorate_if可以使装饰器foo()在被装饰的类上不存在该方法时也不会中断。


4

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

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

我实际上认为这是一个非常有趣的实现。因为它继承了它装饰的类的子类,所以在诸如isinstance检查。

它还有另外一个好处:它并不罕见__init__在自定义声明Django表单进行修改或补充,self.fields因此,最好的改变self.fields发生后,所有的__init__已经跑了有问题的类。

非常聪明。

但是,在您的类中,您实际上希望装饰更改构造函数,但我认为这不是类装饰器的好用例。


0

这是一个示例,它回答了返回类参数的问题。而且,它仍然尊重继承链,即仅返回类本身的参数。get_params作为一个简单的示例,添加了该功能,但是借助inspect模块,可以添加其他功能。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.