Search code examples
pythonpython-decoratorspython-descriptors

Using descriptors in a superclass to avoid code duplication in subclasses


In python, I have a base class from which I have derived a set of subclasses. Each subclass has some subclass-specific functions and a number of unique properties, which have getters and setters defined using @property decorators. But if I have many properties, that leads to lots of code duplication, since the getters and setters all have the same form. Here's a rough example sketch, using database access as an example:

class BaseClass():
    def __init__(self):
        self.handle = None

    def write(self, **kwargs):
        self.handle.write(**kwargs)

class subClass_1(BaseClass):
    def __init__(self):
        self.handle = db_connector("table1")

    @property
    def property1a(self):
        return self.handle.read('columnX')

    @property1a.setter
    def property1a(self, data):
        self.write(columnX=data)

    @property
    def property1b(self):
        return self.handle.read('columnY')

    @property1b.setter
    def property1b(self, data):
        self.write(columnY=data)

    def some_bespoke_function():
        pass

class subClass_2(BaseClass):
    def __init__(self):
        self.handle = db_connector("table2")

    @property
    def property2a(self):
        return self.handle.read('columnZ')

    @property2a.setter
    def property2a(self, data):
        self.write(columnZ=data)

    def another_bespoke_function():
        pass

is there any way I can define something in the BaseClass to avoid having to write all those @property decorators? Ideally, I'd just define something in the subclass like self.mapping = {'property1a':'columnX', 'property1a':'columnX'} for SubClass_1 and self.mapping = {'property2a':'columnZ'} for SubClass_2.


Solution

  • Answering my own question: the solution seems to be to use __getattr__ in the base class, which (I didn't realise) is only called if the attribute does not exist. So the key here is (strangely) not to define the properties at all, but trap reading and writing calls to these non-existing "properties", and fire off the function as needed. No need for decorators at all:

    class BaseClass():
        col_mapping = {}
        def __init__(self):
            self.handle = None
    
        def __getattr__(self, name):
            if name in col_mapping:
                return self.handle.read(col_mapping[name])
            raise AttributeError
    
        def __setattr__(self, name, value):
            if name in col_mapping:
                self.handle.write(**{col_mapping[name]:value})
            else:
                object.__setattr__(self, name, value)
    
        def write(self, **kwargs):
            self.handle.write(**kwargs)
    
    class subClass_1(BaseClass):
        col_mapping = {
            'property1a':'columnX',
            'property1b':'columnY'
        }
    
        def __init__(self):
            self.handle = db_connector("table1")
    
        def some_bespoke_function():
            pass
    
    class subClass_2(BaseClass):
        col_mapping = {
            'property2a':'columnZ'
        }
        def __init__(self):
            self.handle = db_connector("table2")
    
        def another_bespoke_function():
            pass