voodoo: modifying class methods a posteriori

Paul Winkler slinkp23 at yahoo.com
Wed Sep 19 21:19:13 CEST 2001


On 19 Sep 2001 05:49:24 -0700, Hung Jung Lu <hungjunglu at yahoo.com> wrote:
>slinkp23 at yahoo.com (Paul Winkler) wrote:
>> This is exactly what the Decorator design pattern is for.  
>
>I don't think so. :)

OK, I misunderstood a key part of your requirements. The decorator
does not do what you asked for. Sorry. See below.

>(1) In real-life programming, what I have explained is a real need.

Did I say or imply otherwise?

> In
>an actual commercial software development, you typically have many
>people collaborating, and you use third-party modules. Everything is
>moving at the same time, and some modules are not under your control.
>So, very often, it's best practice not to touch other people's codes.

I did not at any time suggest touching anyone else's code.

>Now, the other people have a whole package of many modules and classes
>and subclasses, and you want to modify the behavior of a superclass,
>at some branch of the complicated class hierarchy structure. 

So you saying you want to modify the behavior of a bunch of subclasses
in one fell swoop, by dynamically modifying the code for one method in
a superclass? That's pretty cool. But you can't assume that it will
work for all subclasses. It only works if the subclass does not
overload the method in question. For example:

>>> class A:
...   def f(self): print "hello"
... 
>>> def g(self):
...    print "before"
...    self.f_old()
...    print "after"
...
>>> class B(A):
...   def f(self):
...      print "hello from B"
...
>>> a = A()
>>> b = B()
>>> a.f()
hello
>>> b.f()
hello from B
>>> import new
>>> A.f_old = A.f
>>> A.f = new.instancemethod(g, None, A)
>>> a.f()
before
hello
after
>>> b.f()
hello from B

So in this case you'd have to do to B what you did to A.

Also, I suspect your approach is not thread-safe if you sometimes need
the new behavior and sometimes need the old behavior. I haven't worked
with threads, so I'm not sure about this, but I assume that class
methods would be shared across threads. But maybe these concerns
aren't relevant for your project.

>What you
>have described is nothing but a more elaborate way of class
>derivation, and it does not help, because you'll have to copy and
>re-do the whole class hierarchy tree, when all what you wanted was to
>modify one single method in the superclass.

If you need to modify the behavior of all existing and future
instances of some class, then I agree that the decorator cannot do
this. It doesn't modify classes.

If you just want to modify what some particular objects do at
particular places in your code, which is what I thought you were
doing, the decorator does so with great flexibility. One decorator
class will work transparently for any object that provides the
appropriate method, regardless of what classes it may be derived
from.

# begin example
class WriteNewlineDecorator(Decorator):
    """A silly example of decorating things with a write method."""
    def write(self, astring):
        self._decorated.write(astring)
	self._decorated.write('\n')

# let's try it
import cStringIO
s = cStringIO.StringIO()
fname = '/tmp/blah'
f = open(fname, 'w')
for thing in (s, f):
    for word in ("no", "new", "lines", "here"):
        thing.write(word)
    oldthing = thing
    thing = WriteNewlineDecorator(thing)
    for word in ("now", "there", "will", "be", "new", "lines"):
        thing.write(word)
    thing.writelines(["Old methods such as writelines\n",
                      "will still work fine\n"])
    thing = oldthing
    for word in ("now", "back", "to", "normal", "writing"):
        thing.write(word)
    thing.flush()

f.close()
f = open(fname, 'r')
print "s:\n" + s.getvalue()
print "\nf:\n" + f.read()
# end example

This example will print:

s:
nonewlinesherenow
there
will
be
new
lines
Old methods such as writelines
will still work fine
nowbacktonormalwriting

f:
nonewlinesherenow
there
will
be
new
lines
Old methods such as writelines
will still work fine
nowbacktonormalwriting

> In other words, you cannot
>modify the behavior of existing objects in your approach: you are only
>introducing new classes, new objects.

Of course you can modify the behavior of existing objects. That's the
whole point. You just have to specify which objects. If you need to
refer to it by the same name as before, no problem.
foo=MyDecorator(foo).  If you then want to revert to the old behavior,
you'll have to keep a reference to foo. No surprise there.

It doesn't matter that the decorator is not the same object as the
decoratee. With the Decorator implementation I posted, there are
AFAIKT only these differences:

1) isinstance(foo, A) == 0 

This can (and should) be taken care of by subclassing Decorator from A
(or a superclass of A). This is, in fact, specified as part of the
Decorator implementation; the code I posted was lax in that
regard. Mea culpa.

2) if you kept a reference to the un-decorated object, it will not
test equal to the decorated object. That might be a good thing, but
you can change it if desired by overloading the comparison operators
in the Decorator class, e.g.:

    def __eq__(self, other):
	# optionally put more elaborate comparisons here
        return self._decorated is other

3) foo.__class != "__modulename__.A"
Comment: __class is likely to be unpredictable anyway.

4) foo's address changes (i.e. it will test differently with "is")
Comment: "is-ness" is likely to be unpredictable anyway.

5) "_decorated" in dir(foo) == 1
Comment: dir() is likely to be unpredictable anyway.


--PW



More information about the Python-list mailing list