michael
michael

Reputation: 1767

Different instance-method behavior between Python 2.5 and 2.6

Trying to change the __unicode__ method on an instance after it's created produces different results on Python 2.5 and 2.6.

Here's a test script:

class Dummy(object):

    def __unicode__(self):
        return u'one'

    def two(self):
        return u'two'

d = Dummy()
print unicode(d)
d.__unicode__ = d.two
print unicode(d)
print d.__unicode__()

On Python 2.5, this produces

one
two
two

That is, changing the instance's __unicode__ also changes unicode(instance)

On Python 2.6, this produces

one
one
two

So, after a change, unicode(instance) and instance.__unicode__() return different results.

Why? How can I get this working on Python 2.6?

(For what it's worth, the use case here is that I want to append something to the output of __unicode__ for all subclasses of a given class, without having to modify the code of the subclasses.)

Edit to make the use case a little clearer

I have Class A, which has many subclasses. Those subclasses define simple __unicode__ methods. I want to add logic so that, for instances of a Class A subclass, unicode(instance) gets something tacked on to the end. To keep the code simple, and because there are many subclasses I don't want to change, I'd prefer to avoid editing subclass code.

This is actually existing code that works in Python 2.5. It's something like this:

class A(object):

    def __init__(self):
        self._original_unicode = self.__unicode__
        self.__unicode__ = self.augmented_unicode

    def augmented_unicode(self):
        return self._original_unicode() + u' EXTRA'

It's this code that no longer works on 2.6. Any suggestions on how to achieve this without modifying subclass code? (If the answer involves metaclasses, note that class A is itself a subclass of another class -- django.db.models.Model -- with a pretty elaborate metaclass.)

Upvotes: 4

Views: 253

Answers (3)

Dan Breslau
Dan Breslau

Reputation: 11522

It appears that you are not allowed to monkey-patch protocol methods (those that begin and end with double underscores) :

Note

In practise there is another exception that we haven't handled here. Although you can override methods with instance attributes (very useful for monkey patching methods for test purposes) you can't do this with the Python protocol methods. These are the 'magic methods' whose names begin and end with double underscores. When invoked by the Python interpreter they are looked up directly on the class and not on the instance (however if you look them up directly - e.g. x.repr - normal attribute lookup rules apply).

That being the case, you may be stuck unless you can go with ~unutbu's answer.

EDIT: Or, you can have the base class __unicode__ method search the instance object's dict for a __unicode__ attribute. If it's present, then __unicode__ is defined on the instance object, and the class method calls the instance method. Otherwise, we fall back to the class definition of __unicode__.

I think that this could allow your existing subclass code to work without any changes. However, it gets ugly if the derived class wants to invoke the class implementation -- you need to be careful to avoid infinite loops. I haven't implemented such hacks in this example; merely commented about them.

import types

class Dummy(object):
    def __unicode__(self):
        func = self.__dict__.get("__unicode__", None)
        if func:
            // WARNING: if func() invokes this __unicode__ method directly,
            // an infinite loop could result. You may need an ugly hack to guard
            // against this. (E.g., set a flag on entry / unset the flag on exit,
            // using a try/finally to protect against exceptions.)

            return func()

        return u'one'

    def two(self):
        return u'two'

d = Dummy()
print unicode(d)
funcType = type(Dummy.__unicode__)
d.__unicode__ = types.MethodType(Dummy.two, d)
print unicode(d)
print d.__unicode__()

Testing with Python 2.6 produces the following output:

> python dummy.py 
one
two
two

Upvotes: 2

michael
michael

Reputation: 1767

Looks like Dan is correct about monkey-patching protocol methods, and that this was a change between Python 2.5 and Python 2.6.

My fix ended up being making the change on the classes rather the instances:

class A(object):
    def __init__(self):
        self.__class__.__unicode__ = self.__class__.augmented_unicode

Upvotes: 0

unutbu
unutbu

Reputation: 879919

Edit: In response to the OP's comment: Adding a layer of indirection can allow you to change the behavior of unicode on a per-instance basis:

class Dummy(object):

    def __unicode__(self):
        return self._unicode()

    def _unicode(self):
        return u'one'

    def two(self):
        return u'two'

d = Dummy()
print unicode(d)
# one
d._unicode = d.two
print unicode(d)
# two
print d.__unicode__()
# two

Upvotes: 2

Related Questions