3

I have a class as follows:

class A:
    def __init__(self):
        pass

    def add_attr(self, name):
        setattr(self, name, 'something')

How do I define custom setter, getter for self.name? I cannot use __setattr__, __getattribute__ because that will change the behaviour of add_attr too.

EDIT: the users of this class will add arbitrary number of attributes with arbitrary names:

a = A()
a.add_attr('attr1')
a.add_attr('attr2')

I want custom behavior for only these user added attributes.

Priyatham
  • 2,821
  • 1
  • 19
  • 33

4 Answers4

2

Building off @Devesh Kumar Singh’s answer, I would implement it in some way like this:

class A:
    def __init__(self):
        self.attrs = {}

    def __setattr__(self, key, value):
        if key in self.attrs:
            self.set_attr(key, value)
        else:
            object.__setattr__(self, key, value)

    def __getattribute__(self, key):
         if key in self.__dict__.get(attrs, {}):
             return self.__dict__['get_attr'](self, key)
         return object.__getattribute__(self, key)

    def get_attr(self, key):
        r = self.attrs[key]
        # logic
        return r

    def set_attr(self, key, value):
        # logic
        self.attrs[key] = value

    def add_attr(self, key, value=None):
        self.attrs[key] = value 

add_attr is only used to initialise the variable the first time. You could also edit __setattr__ to set all new attributes in the self.attrs rather than self.__dict__

N Chauhan
  • 3,407
  • 2
  • 7
  • 21
  • Have you tried running this? I think this raises `RecursionError`. Because you are checking for membership in `self.attrs` which itself calls `__getattr__` – Priyatham Jun 04 '19 at 06:19
2

Custom getter and setter logic? That's what a property is made for. Usually these are used to magically mask function calls and make them look like attribute access

class MyDoubler(object):
    def __init__(self, x):
        self._x = x

    @property
    def x(self):
        return x * 2

    @x.setter
    def x(self, value):
        self._x = value

>>> md = MyDoubler(10)
>>> md.x
20
>>> md.x = 20
>>> md.x
40
>>> md._x
20

But there's no rule saying you can't abuse that power to add custom behavior to your getters and setters.

class A(object):
    def __init__(self):
        pass

    @staticmethod
    def default_getter_factory(name):
        def default_getter(self):
            return self.name
        return default_getter

    @staticmethod
    def default_setter_factory(name):
        def default_setter(self, value):
            setattr(self, name, value)
        return default_setter

    def add_attr(self, name, getterfactory=None, setterfactory=None):
        private_name = f"_{name}"

        if getterfactory is None:
            getterfactory = self.__class__.default_getter_factory
        if setterfactory is None:
            setterfactory = self.__class__.default_setter_factory

        getter, setter = getterfactory(private_name), setterfactory(private_name)

        getter = property(getter)
        setattr(self.__class__, name, getter)
        setattr(self.__class__, name, getter.setter(setter))

That said this is all a bit silly, and chances are that whatever it is you're trying to do is a thing that shouldn't be done. Dynamic programming is all well and good, but if I were to review code that did this, I would think very long and hard about alternative solutions before approving it. This reeks of technical debt to me.

Adam Smith
  • 52,157
  • 12
  • 73
  • 112
  • No the user doesn't add their own getter, setter logic. It is just different from default behaviour but consistent across all attributes added. Can you please see my answer and tell me if it is/isn't good programming practice? – Priyatham Jun 04 '19 at 08:33
1

One possibility I could think of is to have a dictionary of dynamic attributes, and set and get the dynamic attributes using the dictionary

class A:
    def __init__(self):
        #Dictionary of attributes
        self.attrs = {}

    #Set attribute
    def set_attr(self, name):
        self.attrs[name] = 'something'

    #Get attribute
    def get_attr(self, name):
        return self.attrs.get(name)

a = A()
a.set_attr('var')
print(a.get_attr('var'))

The output will be something

Or an alternate is to use property decorator to add arguments explicitly outside the class, as described here

class A:
    def __init__(self):
        pass

a = A()
#Add attributes via property decorator
a.attr_1 = property(lambda self: self.attr_1)
a.attr_2 = property(lambda self: self.attr_2)

#Assign them values and print them
a.attr_1 = 4
a.attr_2 = 6

print(a.attr_1, a.attr_2)

The output will be 4 6

Devesh Kumar Singh
  • 20,259
  • 5
  • 21
  • 40
1

I am gonna answer my own question just for reference. This is based on others' answers here. The idea is to use default __setattr__ and __getattribute__ on attributes not added through add_attr.

class A:
    def __init__(self):
        self.attrs = {}

    def add_attr(self, name):
        self.attrs[name] = 'something'

    def __getattribute__(self, name):
        try:
            object.__getattribute__(self, 'attrs')[name]  # valid only if added by user
            # custom logic and return 
        except (KeyError, AttributeError):
           return object.__getattribute__(self, name)

    def __setattr__(self, name, val):
        # similar to __getattribute__
Priyatham
  • 2,821
  • 1
  • 19
  • 33