How to subclass str in Python

2020-02-18 21:01发布

问题:

I am trying to subclass str object, and add couple of methods to it. My main purpose is to learn how to do it. Where I am stuck is, am I supposed to subclass string in a metaclass, and create my class with that meta, or subclass str directly?

And also, I guess I need to implement __new__() somehow, because, my custom methods will modify my string object, and will return new mystr obj.

My class's methods, should be completely chainable with str methods, and should always return a new my class instance when custom methods modified it. I want to be able to do something like this:

a = mystr("something")
b = a.lower().mycustommethod().myothercustommethod().capitalize()
issubclass(b,mystr) # True

I want to have it all the abilities that a str have. For example, a = mystr("something") then I want to use it like, a.capitalize().mycustommethod().lower()

It is my understanding that, I need to implement __new__(). I think so because, strings methods would probably try to create new str instances. So , if I overwrite __new__(), They supposedly would return my custom str class. However, I don't know how to pass arguments to my custom class's __init__() method in that case. And I guess I would need to use type() in order to create a new instance in __new__() method right?

回答1:

Overwriting __new__() works if you want to modify the string on construction:

class caps(str):
   def __new__(cls, content):
      return str.__new__(cls, content.upper())

But if you just want to add new methods, you don't even have to touch the constructor:

class text(str):
   def duplicate(self):
      return text(self + self)

Note that the inherited methods, like for example upper() will still return a normal str, not text.



回答2:

I'm kinda horrified by the complexity of the other answers, and so is python standard library. You can use collections.UserString to subclass string and do not mess with proxying str's methods.

Just subclass it, and add your methods. self.data contains the actual string that is being represented by your object, so you can even implement str-"mutating" methods by reassigning self.data internally.

An example.



回答3:

I am trying to subclass str object, and add couple of methods to it. My main purpose is to learn how to do it.

Don't hardcode the method to the parent class (like the top answer does). Instead, use super like this to support Python 2:

class Caps(str):
    def __new__(cls, content):
        return super(Caps, cls).__new__(cls, content.upper())

In Python 3, it is more performant to call super like this, but it is not backwards compatible with Python 2:

class Caps(str):
    def __new__(cls, content):
        return super().__new__(cls, content.upper())

Usage:

>>> Caps('foo')
'FOO'
>>> isinstance(Caps('foo'), Caps)
True
>>> isinstance(Caps('foo'), str)
True

The complete answer

None of the answers so far does what you've requested here:

My class's methods, should be completely chainable with str methods, and should always return a new my class instance when custom methods modified it. I want to be able to do something like this:

a = mystr("something")
b = a.lower().mycustommethod().myothercustommethod().capitalize()
issubclass(b,mystr) # True

(I believe you mean isinstance(), not issubclass().)

You need a way to intercept the string methods. __getattribute__ does this.

class Caps(str):
    def __new__(cls, content):
        return super(Caps, cls).__new__(cls, content.upper())
    def __repr__(self):
        """A repr is useful for debugging"""
        return f'{type(self).__name__}({super().__repr__()})'
    def __getattribute__(self, name):
        if name in dir(str): # only handle str methods here
            def method(self, *args, **kwargs):
                value = getattr(super(), name)(*args, **kwargs)
                # not every string method returns a str:
                if isinstance(value, str):
                    return type(self)(value)  
                elif isinstance(value, list):
                    return [type(self)(i) for i in value]
                elif isinstance(value, tuple):
                    return tuple(type(self)(i) for i in value)
                else: # dict, bool, or int
                    return value
            return method.__get__(self) # bound method 
        else: # delegate to parent
            return super().__getattribute__(name)
    def mycustommethod(self): # shout
        return type(self)(self + '!')
    def myothercustommethod(self): # shout harder
        return type(self)(self + '!!')

and now:

>>> a = Caps("something")
>>> a.lower()
Caps('SOMETHING')
>>> a.casefold()
Caps('SOMETHING')
>>> a.swapcase()
Caps('SOMETHING')
>>> a.index('T')
4
>>> a.strip().split('E')
[Caps('SOM'), Caps('THING')]

And the case requested works:

>>> a.lower().mycustommethod().myothercustommethod().capitalize()
Caps('SOMETHING!!!')


回答4:

Here's a quick hack to do what you want: you basically intercept every function call, and, if you see that it's returning a string, you convert it back to your own class type.

While this works in this simple example, it has some limitations. Among other things, operators such as the subscript operator are apparently not handled.

class FunWrapper(object):
    def __init__(self, attr):
        self.attr = attr

    def __call__(self, *params, **args):
        ret = self.attr(*params, **args)
        if type(ret) is str:
            return Foo(ret)
        return ret

class Foo(object):
    def __init__(self, string):
        self.string = string

    def __getattr__(self, attr):
        return FunWrapper(getattr(self.string, attr))

    def newMethod(self):
        return "*%s*" % self.string.upper()


f = Foo('hello')
print f.upper().newMethod().lower()


回答5:

You can try something like:

class mystr(str):
    def new_method(self):
        pass

but you won't be sure that standard methods will return a 'mystr' instance too