2

See my answer below; after posting the question I realized what was going on.


My attempt to override the string representation of a class using a decorator isn't working. I must be missing something but can't figure out what it is.

from functools import wraps

def str_dec(obj):
    @wraps(obj.__str__)
    def __str__(cls):
        return '<desired result>'
    obj.__str__ = __str__
    return obj

@str_dec
class Test():
    pass

assert Test.__str__(Test) == '<desired result>' # success
assert str(Test) == '<desired result>' # failure

Obviously the new Test.__str__ method is being created successfully, but I was expecting it to be called by the str function. Instead, str is using the default string creation method (for type objects).

I can get str working using a metaclass, but then the __str__ method doesn't work! And I would prefer to use a decorator anyway.

class str_meta(type):
    def __str__(cls):
        return '<desired result>'

class Test(metaclass = str_meta):
    pass

assert str(Test) == '<desired result>' # success
assert Test.__str__(Test) == '<desired result>' # failure

What is going on here? How can I override __str__ consistently for my class?

Rick
  • 43,029
  • 15
  • 76
  • 119

2 Answers2

2

In short, the __str__ method affects the behaviour of class instances, not the class type.

For your first example, the following does work:

assert str(Test()) == '<desired result>'
assert str(Test) == type.__str__(Test)

Where the stringification to want to modify is the type, not its instances, you do need a metaclass as in the second example.

I am curious what the use case for that is though. Can see adding a classmethod returning a str as a named method, but would be surprised by a type that didn't respond to str() as per repr().

gz.
  • 6,661
  • 1
  • 23
  • 34
  • [Here is the use case](http://stackoverflow.com/questions/44011851/dynamically-subclass-an-enum-base-class). – Rick May 17 '17 at 01:12
2

The first example isn't working because the __str__ method I have added is not going to show up in the MRO for Test, only for instances of Test. Not sure why I didn't see that before.

In short, my decorator as written creates a class method, not a metaclass method.

My final test should be:

assert type(Test).__str__(Test) == '<desired result>' # success

It turns out that, even doing things correctly, I will not be able to successfully bolt on a new __str__ method to my class using just a decorator:

from functools import wraps

def str_dec(obj):
    @wraps(type(obj).__str__)
    def __str__(cls):
        return '<desired result>'
    # this line causes an error:
    type(obj).__str__ = __str__
    return obj

@str_dec
class Test():
    pass

The reason for this is that the type for the class Test is in fact type, and you cannot override methods of type. Instead, you must subclass type by creating a metaclass and override that metaclass' __str__ method.

So if I still want to use a decorator, I can, but I need a do-nothing metaclass:

class DoNothingMeta(type):
    pass

@str_dec
class Test(metaclass=DoNothingMeta):
    pass

assert Test.__str__(Test) == '<desired result>' # success
assert str(Test) == '<desired result>' # success

Now the decorator will work as written because type(obj) will evaluate to type(Test) and return DoNothingMeta instead of type, and it is perfectly legal to override dunder methods for a custom metaclass.

Rick
  • 43,029
  • 15
  • 76
  • 119