Tuesday, August 10, 2010

Decorators Revisited

I was saying the other day that I've finally understood decorators after reading that response from Stack Overflow, but there was still something I was unsure of. I was wondering if you can make class decorators, not just function decorators, and today I found out you can. I did a little bit of experimenting and I came up with this:

>>> class Foo(object):
        def __new__(cls, kls):
            class Bar(object):
                def __init__(self, length, width, height):
                    try:
                        self.length = kls.length
                    except AttributeError:
                       self.length = length
                    try:
                       self.width = kls.width
                    except AttributeError:
                       self.width = width
                    self.heigth = height
            return Bar


>>> @Foo
class Buzz(object):
    def __init__(self, length, width):
        self.length = length
        self.width = width


>>> Buzz(1, 2, 3)
<__main__.Bar object at 0x00BD5390>
>>> b = Buzz(1, 2, 3)
>>> b.length
1
>>> b.width
2
>>> b.height
3

As you can see, 'b' is an instance of Bar, but that can be fixed by decorating Bar with the wraps decorator from the functools module.

Hmm, I just realized I'm turning class attributes into instance attributes with this code which is not something someone usually wants. I also realized that functools.wraps needs to be passed some extra arguments to work on classes, because the __doc__ attribute is read only and dictproxy objects don't have an update method.

This causes something which might become a problem: the wrapped object's dict contains the wrapper's class attributes (the ones used to preserve the default values).

Anyway, here's a different version that keeps instance attributes the way they are and also preserves their default values:

>>> class Foo(object):
        def __new__(self, kls):
        @functools.wraps(kls, assigned=('__module__', '__name__'), updated=())
        class Bar(object):
            try:
                length = kls().length
            except AttributeError:
                length = 0
            try:
                width = kls().width
            except AttributeError:
                width = 0
            def __init__(self, height=0, length=length, width=width):
                self.length = length
                self.width = width
                self.height = height
        return Bar

>>> @Foo
class Derp(object):
    def __init__(self, length=8, width=9):
        self.length = length
        self.width = width


>>> Derp.__name__
'Derp'

It's not very pretty, and I can't think of a use case right now, but it's interesting as an experiment.

No comments:

Post a Comment