How to subclass str in Python

后端 未结 5 2045
离开以前
离开以前 2020-12-24 07:09

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 met

相关标签:
5条回答
  • 2020-12-24 07:29

    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()
    
    0 讨论(0)
  • 2020-12-24 07:34

    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.

    0 讨论(0)
  • 2020-12-24 07:35

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

    UserString was created before it was possible to subclass str directly, so prefer to subclass str, instead of using UserString (as another answer suggests).

    When subclassing immutable objects, it's usually necessary to modify the data before you instantiate the object - therefore you need to both implement __new__ and call the parent __new__ (preferably with super, instead of str.__new__ as another answer suggests).

    In Python 3, it is more performant to call super like this:

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

    __new__ looks like a class method, but it is actually implemented as a static method, so we need to pass cls redundantly as the first argument. We don't need the @staticmethod decorator, however.

    If we use super like this to support Python 2, we'll note the redundant cls more clearly:

    class Caps(str):
        def __new__(cls, content):
            return super(Caps, cls).__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 emphasized textthis.

    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!!!')
    

    Response to Comment

    Why is the Python 3 only call, i.e. super().method(arg) more performant?

    The function already has access to both __class__ and self without doing a global and local lookup:

    class Demo:
        def foo(self):
            print(locals())
            print(__class__)
    
    >>> Demo().foo()
    {'self': <__main__.Demo object at 0x7fbcb0485d90>, '__class__': <class '__main__.Demo'>}
    <class '__main__.Demo'>
    

    See the source for more insight.

    0 讨论(0)
  • 2020-12-24 07:50

    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

    0 讨论(0)
  • 2020-12-24 07:51

    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.

    0 讨论(0)
提交回复
热议问题