You can use descriptors. Descriptors are, in layman's terms, reusable properties. The advantage over the __getattr__ and __setattr__ hooks is that you have more fine-grained control over what attributes are managed by descriptors.
class MyDescriptor:
    def __init__(self, default='default'):
        self.default = default
    def __set_name__(self, owner, name): # new in Python3.6
        self.name = name
    def __get__(self, instance, owner):
        print('getting {} on {}'.format(self.name, instance))
        # your getter logic here
        # dummy implementation:
        if instance is not None:
            try:
                return vars(instance)[self.name]
            except KeyError:
                return self.default
        return self
    def __set__(self, instance, value):
        print('setting {} on {}'.format(self.name, instance))
        # your getter logic here
        # dummy implementation:
        vars(instance)[self.name] = value
class MyClass:
    a = MyDescriptor()
    b = MyDescriptor()
    _id = 1
    # some logic for demo __repr__
    def __init__(self):
        self.c = 'non-descriptor-handled'
        self.id = MyClass._id
        MyClass._id += 1
    def __repr__(self):
        return 'MyClass #{}'.format(self.id)
Demo:
>>> m1 = MyClass()
>>> m2 = MyClass()
>>> m1.c
'non-descriptor-handled'
>>> m1.a
getting a on MyClass #1
'default'
>>> m1.b
getting b on MyClass #1
'default'
>>> m1.b = 15 
setting b on MyClass #1
>>> m1.b
getting b on MyClass #1
15
>>> m2.b
getting b on MyClass #2
'default'