user3076813
user3076813

Reputation: 519

Capturing the external modification of a mutable python object serving as an instance class variable

I am trying to track the external modification of entries of a mutable python object (e.g., a list tor dictionary). This ability is particularly helpful in the following two situations:

1) When one would like to avoid the assignment of unwanted values to the mutable python object. Here's a simple example where x must be a list of integers only:

class foo(object):
    def __init__(self,x):
        self.x = x
    def __setattr__(self,attr_name,attr_value):
        # x must be a list of integers only
        if attr_name == 'x' and not isinstance(attr_value,list):
            raise TypeError('x must be a list!')
        elif attr_name == 'x' and len([a for a in attr_value if not isinstance(a,int)]) > 0:
            raise TypeError('x must be a list of integers only')
        self.__dict__[attr_name] = attr_value

# The following works fine and it throws an error because x has a non-integer entry
f = foo(x = ['1',2,3])

# The following assigns an authorized list to x
f = foo(x = [1,2,3])

# However, the following does not throw any error. 
#** I'd like my code to throw an error whenever a non-integer value is assigned to an element of x
f.x[0] = '1'
print 'f.x = ',f.x

2) When one needs to update a number of other variables after modifying the mutable Python object. Here's an example, where x is a dictionary and x_vals needs to get updated whenever any changes (such as deleting an entry or assigning a new value for a particular key) are made to x :

class foo(object):
    def __init__(self,x,y = None):
        self.set_x(x)
        self.y = y

    def set_x(self,x):
        """
        x has to be a dictionary 
        """
        if not isinstance(x,dict):
            raise TypeError('x must be a dicitonary')

        self.__dict__['x'] = x
        self.find_x_vals()

    def find_x_vals(self):
        """
        NOTE: self.x_vals needs to get updated each time one modifies x 
        """ 
        self.x_vals = self.x.values()

    def __setattr__(self,name,value):
        # Any Changes made to x --> NOT SURE HOW TO CODE THIS PART! #
        if name == 'x' or ...:
            raise AttributeError('Use set_x to make changes to x!')
        else:
            self.__dict__[name] = value 

if __name__ == '__main__':
    f = foo(x={'a':1, 'b':2, 'c':3}, y = True)
    print f.x_vals

    # I'd like this to throw an error asking to use set_x so self.x_vals
    # gets updated too
    f.x['a'] = 5

    # checks if x_vals was updated
    print f.x_vals

    # I'd like this to throw an error asking to use set_x so self.x_vals gets updated too
    del f.x['a']
    print f.x_vals

Upvotes: 4

Views: 117

Answers (2)

Ethan Furman
Ethan Furman

Reputation: 69100

You cannot use property because the thing you are trying to protect is mutable, and property only helps with the geting, seting, and deleteing of the object itself, not that objects internal state.

What you could do is create a dict subclass (or just a look-a-like if you only need a couple of the dict abilities) to manage access. Then your custom class could manage the __getitem__, __setitem__, and __delitem__ methods.


Update for question revision

My original answer is still valid -- whether you use property or __getattribute__1 you still have the basic problem: once you hand over the retrieved attribute you have no control over what happens to it nor what it does.

You have two options to work around this:

  1. create subclasses of the classes you want to protect, and put the restrictions in them (from my original answer), or

  2. create a generic wrapper to act as a gateway.

A very rough example of the gateway wrapper:

class Gateway():
    "use this to wrap an object and provide restrictions to it's data"

    def __init__(self, obj, valid_key=None, valid_value=None):
        self.obj = obj
        self.valid_key = valid_key
        self.valid_value = valid_value

    def __setitem__(self, name, value):
        """
        a dictionary can have any value for name, any value for value
        a list will have an integer for name, any value for value
        """
        valid_key = self.valid_key
        valid_value = self.valid_value
        if valid_key is not None:
            if not valid_key(name):
                raise Exception('%r not allowed as key/index' % type(name))
        if valid_value is not None:
            if not valid_value(value):
                raise Exception('%r not allowed as value' % value)
        self.obj[name] = value

and a simple example:

huh = Gateway([1, 2, 3], valid_value=lambda x: isinstance(x, int))
huh[0] = '1'

Traceback (most recent call last):
 ...
Exception: '1' not allowed as value

To use Gateway you will need to override more methods, such as append (for list).


1 Using __getattribute__ is not advised as it is the piece that controls all the aspects of attribute lookup. It is easy to get wrong.

Upvotes: 1

Ilya Peterov
Ilya Peterov

Reputation: 2065

You could make x_vals a property like that:

@property
def x_vals(self):
    return self.x.values()

And it would keep x_vals up to date each time you access it. It would event be faster because you wouldn't have to update it each time you change x.

If your only problem is keeping x_vals up to date, it's going to solve it, and save you the hassle of subclassing stuff.

Upvotes: 2

Related Questions