[portland] class decorators useful?

kirby urner kirby.urner at gmail.com
Thu Apr 16 02:25:41 CEST 2009


I'm thinking of Adam's comment that he never decorated a class and
didn't regret it, and, truth be told, I've not experimented with that
feature much.

So I wanted to give a use case, see if PPUGers think it flies.

Consider I have this Vector class that does the usual stuff around
xyz, don't even have dot or cross product, just add, subtract, negate
and scalar multiply.

Note that to __sub__(v) is simply to __add__(v.__neg__()) i.e. to
subtract v is to add the additive inverse of v.

To allow 3 * v as well as v * 3 (scale by a scalar), we need an
__rmul__ = __mul__:

class Vector:

    def __init__(self, xyz):
            self.xyz = tuple(xyz)

    def __add__(self, other):
            return Vector([i + j for i,j in zip(self.xyz, other.xyz)])

    def __neg__(self):
            return Vector([-i for i in self.xyz])

    def __sub__(self, other):
            return self + (-other)

    def __mul__(self, scalar):
            return Vector([scalar * i for i in self.xyz])

    __rmul__ = __mul__

    def __repr__(self):
            return "Vector (%s)" % (self.xyz,)
	

def test():
    oA = Vector((1,2,3))
    oB = Vector((2,0,2))
    print(oA)
    print(oA + oB)
    print(oA - oB)
    print(-oB)
    print(oA + (-oB))
    print(3 * oA)
    oA.draw()
    oA.erase()

The last two tests fail however, because these refer to Visual Python
methods that will actually put colored cylinders in an OpenGL "fish
tank" (= canvas with z-axis).

Say I don't want to mess with the Vector code, because I use it
elsewhere and consider VPython stuff somewhat extraneous.  I just want
to "tweak" my Vector class with some additional capabilities, for the
purposes of this module.

What I do instead of subclassing Vector and adding draw and erase
methods, is I write a class decorator function that *redefines* my
Vector in terms of itself with this added Mixin class, the latter
containing the stuff from VPython:

[ mixin.py module ]

import faux_visual as visual

def makeVisual(Theclass):

    class Mixin:

        radius = 0.1
        color = 'red'

        def draw(self):
            """define and render the cylinder"""
            v = visual.vector(*self.xyz)
            print("drawing vector (%s, %s, %s)"
                  % self.xyz )
            self.cyl = visual.cylinder(pos = (0,0,0),
                    axis = v, radius = self.radius, color = self.color)

        def erase(self):
            """toss the cylinder"""
            if self.cyl:
                print("erasing vector (%s, %s, %s)" % self.xyz)
                self.cyl.visible = 0
            self.cyl = None

    class Newclass(Theclass, Mixin):
        pass

    return Newclass

.... then same Vector as before, plus the tests.

Note that Python 3.x doesn't yet have VPython for a 3rd party option,
so I had to write a "stupid stub" to test my code:

[ saved separately as faux_visual.py, note lowercase vector, like in
visual (VPython) ]

class vector:

    def __init__(self, x, y, z):
        self.x = x; self.y = y; self.z = z

class cylinder:  # faux cylinder, takes named args

    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

Now all I have to do in my original module is:

@makeVisual
class Vector:

    def __init__(self, xyz):
            self.xyz = tuple(xyz)

    def __add__(self, other):
            return Vector([i + j for i,j in zip(self.xyz, other.xyz)])

    def __neg__(self):
            return Vector([-i for i in self.xyz])

    def __sub__(self, other):
            return self + (-other)

    def __mul__(self, scalar):
            return Vector([scalar * i for i in self.xyz])

    __rmul__ = __mul__

    def __repr__(self):
            return "Vector (%s)" % (self.xyz,)
	
i.e. by adding this decorator above my Vector class (which I didn't
change in any other way), I am, for the purposes of this module,
automatically enhancing its API with Vpython stuff.

I'm still able to use all my original tests, don't need a new name for
my class.  However, draw and erase now work:

Running the mixin.py module:

>>>
Vector ((1, 2, 3))
Vector ((3, 2, 5))
Vector ((-1, 2, 1))
Vector ((-2, 0, -2))
Vector ((-1, 2, 1))
Vector ((3, 6, 9))
drawing vector (1, 2, 3)
erasing vector (1, 2, 3)

Is this persuasive at all, as a possible design pattern?

Basically you're wanting to not mess with a namespace, yet want to
retrofit a class with some additional capabilities.  A decorator
returns the same name, so the code that didn't know about the
enhancements before, doesn't need to know now, and yet some new tests
are passed.

There are other ways to accomplish the above.

At least I'm showing how a decorator might accomplish something far
less ambitious than developing a metaclass.

Here's an excellent talk on class decorators, also talked to this guy
(Jack Diederick) at the CPP / Iceland party (lots of Twisted guys):

http://pycon.blip.tv/file/1957258/


Kirby

Note:  I see my stuff is starting to come thru, though I don't get how
a 3 hour talk fragments into a 6 minute anything.
http://pycon.blip.tv/#2008102 (yow! part 1 is 57 minutes, will my
mplayer plugin even work?).

This talk by Jeff Rush on namespaces vs. code objects was spectacular,
recommended!
http://pycon.blip.tv/file/1957258/


More information about the Portland mailing list