4

I can do this:

class Person:
    def __init__(self, firstName, lastName):
        self.firstName = firstName
        self.lastName = lastName

    def __str__(self):
        print self.firstName

bob = Person('Robert', 'Smith')
print bob
>>> Robert

But I want to do it more generically rather than overriding __str__ for every class manually. I've tried using decorators and metaclasses, but I am confounded. I think that it should be possible to do something similar to this, but I don't know how to make the decorator or metaclass accept an argument and still create a properly instantiable object:

class strAs:

    def __init__(self, prop):
        self.prop = prop

    def __call__(self, cls):
        decoratorSelf = self
        def wrapper(*args, **kwargs):
            def __str__(s):
                return getattr(s, decoratorSelf.prop)

            c = cls(*args, **kwargs)
            c.__str__ = __str__

            return c
        return wrapper


@strAs(prop='firstName')
class Person:
    def __init__(self, firstName, lastName):
        self.firstName = firstName
        self.lastName = lastName

@strAs(prop='name')
class Dog:
    def __init__(self, name):
        self.name = name

bob = Person('Robert', 'Smith')
fido = Dog('Fido')

print bob
print fido

>>> Robert
Fido

But this fails with:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/tmp/py6495xVN", line 33, in <module>
    print bob
TypeError: __str__() takes exactly 1 argument (0 given)

I can't figure out why __str__() is not getting self passed to it. I noticed that the bob instance of the Person class seems to be missing some typical object methods, so I tried I subclassing object like this (and added two print statements):

class strAs(object):

    def __init__(self, prop):
        self.prop = prop

    def __call__(self, cls):
        decoratorSelf = self
        def wrapper(*args, **kwargs):
            print 'in wrapper'
            def __newstr__(s):
                print 'in newstr'
                return getattr(s, decoratorSelf.prop)

            c = cls(*args, **kwargs)
            c.__str__ = __newstr__

            return c
        return wrapper


@strAs(prop='firstName')
class Person(object):
    def __init__(self, firstName, lastName):
        self.firstName = firstName
        self.lastName = lastName

@strAs(prop='name')
class Dog(object):
    def __init__(self, name):
        self.name = name

bob = Person('Robert', 'Smith')
fido = Dog('Fido')

print bob
print fido

And now I get some output with __str__ apparently being called correctly:

>>> in wrapper
in wrapper
<__main__.Person object at 0x7f179389e450>
<__main__.Dog object at 0x7f179389e510>

But it's not printing the property as it should be, and it doesn't appear to even be using the __newstr__ function, because it's not printing in newstr. However, if I check bob.__str__, I see that it is indeed <function __newstr__ at 0x7f17951d78c0>.

I must be missing something obvious, but I feel like this is over my head right now. :/

  • If your question is about how to write decorators that take parameters, you might want to look at [this question](http://stackoverflow.com/questions/5929107/python-decorators-with-parameters). – BrenBarn Feb 20 '15 at 07:24
  • Thanks, I've googled a lot and found some info like that, but what I'm trying to do is decorate a class and replace one of its methods, passing an argument to the constructor that changes the decorated class. I can find plenty of examples of function decorators, function decorators with arguments, and class decorators, but I can't seem to find class decorators (that is, applying decorators to classes, not class-based function decorators) with arguments. I'm sure I'm missing something obvious, but I guess it's not obvious to me. –  Feb 20 '15 at 07:28
  • 1
    The idea is the same. If you read the question I linked, you'll see the answer says: "the decorator with arguments should return a function that will take a function and return another function". The same applies to class decorators; it just means that the decorator with arguments should return a function that will take a class and return another class. In other words, `strAs` needs to return the actual decorator. Try adapting the answer there and asking a more specific question if you can't figure it out. – BrenBarn Feb 20 '15 at 07:44
  • Thanks. I did some rewriting and debugging, but I'm still quite stuck. I guess there must be a lot of Python internals knowledge that I don't have that's necessary to work this out. –  Feb 20 '15 at 08:31

1 Answers1

4

In your case, I'd just let the decorator modify the class. Such as

class strAs:

    def __init__(self, prop):
        self.prop = prop

    def __call__(self, cls):
        prop = self.prop
        def __str__(s):
            return getattr(s, prop)
        cls.__str__ = __str__
        return cls
glglgl
  • 89,107
  • 13
  • 149
  • 217
  • Well, that is simple, and it works. Thank you. Now to figure out why my latest attempt didn't work. It's like I was so close, but I was feeling around in the dark... –  Feb 20 '15 at 08:38
  • @blujay Your last version doesn't work because you are changing the instance's `__str__` instead of the class's one. That doesn't work. Even if it would, it wouldn't take the `self` argument, as it would operate on instance level. – glglgl Feb 20 '15 at 08:50
  • Oh, I thought changing the instance's method was the way it was supposed to work. I have a lot to learn. Thanks for your help. –  Feb 20 '15 at 09:31