views:

561

answers:

4

I'm trying to subclass str, but having some difficulties due to its immutability.

class DerivedClass(str):

    def __new__(cls, string):
        ob = super(DerivedClass, cls).__new__(cls, string)
        return ob

    def upper(self):
        #overridden, new functionality. Return ob of type DerivedClass. Great.
        caps = super(DerivedClass, self).upper()
        return DerivedClass(caps + '123')

derived = DerivedClass('a')

print derived.upper() #'A123'
print type(derived.upper()) #<class '__main__.DerivedClass'>
print derived.lower() #'a' 
print type(derived.lower()) #<type 'str'>

For inherited methods that don't require any new functionality, such as derived.lower(), is there a simple, pythonic way to return an object of type DerivedClass (instead of str)? Or am I stuck manually overriding each str.method(), as I did with derived.upper()?

Any advice is appreciated.

Edit:

#Any massive flaws in the following?

class DerivedClass(str):
    def __new__(cls, string):
        ob = super(DerivedClass, cls).__new__(cls, string)
        return ob

    def upper(self):
        caps = super(DerivedClass, self).upper()
        return DerivedClass(caps + '123')

    def __getattribute__(self, name):
        att = super(DerivedClass, self).__getattribute__(name)

        if not callable(att):
            return att

        def call_me_later(*args, **kwargs):
            result = att(*args, **kwargs)
            if isinstance(result, basestring):
                return DerivedClass(result)
            return result
        return call_me_later
A: 

You might be able to do this by overriding __getattribute__.

def __getattribute__(self, name):
    # Simple hardcoded check for upper.
    # I'm sure there are better ways to get the list of defined methods in
    # your class and see if name is contained in it.
    if name == 'upper':
        return object.__getattribute__(self, name)

    return DerivedClass(object.__getattribute__(self, name))
Zr40
Don't you mean str.__getattribute__? And DerivedClass.__dict__ would tell you which names are in the derived class.
James Hopkin
This seems to throw a TypeError. print derived.lower() TypeError: 'DerivedClass' object is not callable
Yes, I believe it should be str.__getattribute__. Still get the TypeError.
You're getting a TypeError because __getattribute__ returns a reference to the attribute (in this case the lower function)... it doesn't call that function. This function is returning a DerviedClass instance, not the lower function, and since DerivedClass does not implement __call__, you can't call it.
Jarret Hardie
+2  A: 

You can do this by overriding __getattribute__ as Zr40 suggests, but you will need to have getattribute return a callable function. The sample below should give you what you want; it uses the functools.partial wrapper to make life easier, though you could implement it without partial if you like:

from functools import partial

class DerivedClass(str):

    def __new__(cls, string):
        ob = super(DerivedClass, cls).__new__(cls, string)
        return ob

    def upper(self):
        #overridden, new functionality. Return ob of type DerivedClass. Great.
        caps = super(DerivedClass, self).upper()
        return DerivedClass(caps + '123')

    def __getattribute__(self, name):
        func = str.__getattribute__(self, name)
        if name == 'upper':
            return func

        if not callable(func):
            return func

        def call_me_later(*args, **kwargs):
            result = func(*args, **kwargs)
            # Some str functions return lists, ints, etc
            if isinstance(result, basestring:
                return DerivedClass(result)
            return result

        return partial(call_me_later)
Jarret Hardie
+3  A: 

You're both close, but checking for each doesn't extend well to overriding many methods.

from functools import partial

class DerivedClass(str):
    def __new__(cls, string):
        ob = super(DerivedClass, cls).__new__(cls, string)
        return ob

    def upper(self):
        caps = super(DerivedClass, self).upper()
        return DerivedClass(caps + '123')

    def __getattribute__(self, name):
        if name in ['__dict__', '__members__', '__methods__', '__class__']:
            return object.__getattribute__(self, name)
        func = str.__getattribute__(self, name)
        if name in self.__dict__.keys() or not callable(func):
            return func

        def call_me_later(*args, **kwargs):
            result = func(*args, **kwargs)
            # Some str functions return lists, ints, etc
            if isinstance(result, basestring):
                return DerivedClass(result)
            return result

        return partial(call_me_later)
tghw
A good addition, but as written this will produce a recursive call to __getattribute__.
Jarret Hardie
Fixed it by checking for dict
tghw
Good solution! but dir is returning an empty list
Nadia Alramli
+1: I had thought about making the example generic by checking the subclass dict, but decided against it in favour of speed given my schedule today. I'm really pleased to see someone take on the task... thanks!
Jarret Hardie
You can change the dir() issue by changing "if name == '__dict__'" to "if name in ['__dict__', '__members__', '__methods__', '__class__']". Just proves that this kind of extension is a slippery slope :-) BTW, you can investigate this sort of thing by just putting a print statement ("print name") as the first line in "def __getattribute__".
Jarret Hardie
Updated to fix dir, thanks Jarret. I was using the print at the top to debug until it got too annoying with PythonWin's intellisense (though it's cool to see how the IDE figures out what to list).
tghw
Is it necessary to check self.__dict__.keys() for name? The call to str.__getattribute__(self, name) seems to call methods as expected (overridden or not) and 'call_me_later' returns instances of the subclass if appropriate. I assume callable(func) is to catch any attempts to access data members. I've modified the contributions slightly, and edited the question. For the sake of simplicity, since I'm not yet familiar with it, partial is not used. Thoughts? Thanks again :)
@trigue - Put a print statement in __getattribute__. You'll see that it gets called every time through.
tghw
+2  A: 

Good use for a class decorator -- roughly (untested code):

@do_overrides
class Myst(str):
  def upper(self):
    ...&c...

and

def do_overrides(cls):
  done = set(dir(cls))
  base = cls.__bases__[0]
  def wrap(f):
    def wrapper(*a, **k):
      r = f(*a, **k)
      if isinstance(r, base):
        r = cls(r)
      return r
  for m in dir(base):
    if m in done or not callable(m):
      continue
    setattr(cls, m, wrap(getattr(base, m)))
Alex Martelli
Thx Alex. I plan to move onto studying decorators soon (have one of your related talks bookmarked, actually), so I'll keep this usage in mind.