Python Expert: how to inherit built-in class and override every member function w.r.t. the base-class member function?

791 Views Asked by At

It is known that in Python, due to optimization concerns, we cannot add/modify member functions of a built-in class, e.g., adding an sed function to the built-in str class to perform re.sub(). Thus, the only way to achieve so is to inherit the class (or subclassing). i.e.,

class String(str):
    def __init__(self, value='', **kwargs):
        super().__init__()

    def sed(self, src, tgt):
        return String(re.sub(src, tgt, self))

The problem with this is that after sub-classing, member functions return base-class instance instead of the inherited class instance. For example, I would like to chain String edits String(' A b C d E [!] ').sed(...).lower().sed(...).strip().sed('\[.*\]', '').split() and so on. However, functions such as .lower() and .strip() returns an str instead of String, so cannot perform .sed(...) afterwards. And I do not want to keep casting to String after every function call.

So I did a manual over-ride of every base-class methods as follows:

class String(str):
    for func in dir(str):
        if not func.startswith('_'):
            exec(f'{func}=lambda *args, **kwargs: [(String(i) if type(i)==str else i) for i in [str.{func}(*args, **kwargs)]][0]')

    def __init__(self, value='', **kwargs):
        super().__init__()

    def sed(self, src, tgt):
        return String(re.sub(src, tgt, self))

However, not every member function returns a simple str object, e.g., for functions such as .split(), they return a list of str; other functions like .isalpha() or .find() return boolean or integer. In general, I want to add more string-morphing functions and do not want to manually over-ride member functions of each return type in order to return inherited-class objects rather than base-class objects. So is there a more elegant way of doing this? Thanks!

1

There are 1 best solutions below

2
FMc On

Python's built-in classes are not designed to support that style of inheritance easily. Also, the whole idea seems flawed to my eye. Even if you do figure out a way to solve the problem as you've framed it, what's the advantage over good old functions?

# Special String objects with new methods.

s = String('foo bar')
result = s.sed('...', '...')

# Regular str instances passed to ordinary functions.

s = 'foo bar'
result = sed(s, '...', '...')

That said, here's one way to try. I have not tested it extensively, it might have a flaw, and I would never use it in real code. The basic idea is to capture objects returned during low-level attribute access, and if the object is callable return a wrapped version of it that will perform the needed data conversions.

import re
from functools import wraps

class String(str):

    def __getattribute__(self, attr):
        obj = object.__getattribute__(self, attr)
        return wrapped(obj) if callable(obj) else obj

    def __init__(self, value='', **kwargs):
        super().__init__()

    def sed(self, src, tgt):
        return re.sub(src, tgt, self)

def wrapped(func):

    @wraps(func)
    def wrapper(*xs, **kws):
        obj = func(*xs, **kws)
        return convert(obj)

    return wrapper

def convert(obj):
    if isinstance(obj, str):
        return String(obj)
    elif isinstance(obj, list):
        return [convert(x) for x in obj]
    elif isinstance(obj, tuple):
        return tuple(convert(x) for x in obj)
    else:
        return obj

Demo:

s = String('foo bar')
got = s.sed('foo', 'bzz').upper().split()
print(got)
print(type(got))
print(type(got[0]))

Output:

['BZZ', 'BAR']
<class 'list'>
<class '__main__.String'>