Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recreate class object changing one constructor argument

I'd like to have implement an update method in my class that recreates the class but changes just one constructor argument.

My attempt:

class Updateable:
    def update(self, var, var_str, **kwargs):
        kwargs.update(self._vars)
        kwargs[var_str] = var
        self.__init__(**kwargs)

class Rectangle(Updateable):
    def __init__(self, length, perimeter):
        self._vars = locals()
        self.length = length
        self.width = 0.5*(perimeter - 2.*length)      

r = Rectangle(10, 20)
r.update('perimeter', 16)

The problem is, the whole locals() thing, I think, is pretty dodgy, and it means that any class that is Updateable needs to assign self._vars.

What would be the correct way to achieve this functionality? Decorators, metaclasses? Something simpler?

like image 891
Jean-Luc Avatar asked Dec 12 '25 00:12

Jean-Luc


2 Answers

Please correct me if I misunderstood your question, or if you don't want high level advise and your just question solved.

What your __init__ currently does is recalculate the properties of a geometric shape if a (possibly relevant) variable is changed. Step 1 is to take this out of the __init__, and into a seperate def which is called by init. The main thing here is that you do not pass variables to this function, but use the class variables which have been set in either __init__ or one the superclass updating methods.

Step 2 is to change your update function. Python has a form of getters and setters called properties allowing you to hook tasks to updating your variables. On the other hand a more generalized way is more similar to your own update, and is listed as option 2 below

Example alternative

class Updateable:
    # Option 1
    @property
    def perimeter(self):
        return self.__perimeter

    @perimeter.setter
    def perimeter(self, perimeter):
        self.__perimeter = perimeter
        self.recalculate_everything() # or self.calculate_width() or something

    # Option 2
    def update(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)
        self.recalculate_everything

class Rectable(Updateable):
    def __init__(self, length, perimeter):
        self.__length = length
        self.__perimeter = perimeter
        recalculate_everything()

    def recalculate_everything():
        self.calculate_width()
        ...     

    def calculate_width():
        self.__width = 0.5*(self.__perimeter - 2.*self.__length)     
like image 141
Laurens Koppenol Avatar answered Dec 14 '25 14:12

Laurens Koppenol


Laurens Koppenol suggested using properties (Python's generic support for computed attribute) which is a good idea but his example code is both broken in many ways and more complicated than it has to be, so here's a simpler, working and pythonic example (no Updatable class nor any other extraneous stuff required):

class Rectangle(object):
    def __init__(self, length, perimeter):
        self.length = length
        self.perimeter = perimeter

    @property
    def width(self):
        return 0.5*(self.perimeter - 2.*self.length)     

If you want to cache the width value (to avoid useless computations) but still make sure it's updated when length or perimeter change, you'll need to make them all properties:

class Rectangle(object):
    def __init__(self, length, perimeter):
        self.length = length
        self.perimeter = perimeter

    @property
    def length(self):
        return self._length

    @length.setter
    def length(self, value):
        self._length = value
        self._width = None


    @property
    def perimeter(self):
        return self._perimeter

    @length.setter
    def perimiter(self, value):
        self._perimeter = value
        self._width = None

    @property
    def width(self):
        if self._width is None:
            self._width = 0.5*(self.perimeter - 2.*self.length)     
        return self._width

or (if you have a lot of such stuff) use some "cached_property with invalidation" implementation as this one: Storing calculated values in an object

edit: wrt/ your question, the call to locals is indeed ugly (and can easily break - you may have local variables that are not supposed to be parts of _vars), as well as the need to explicitely set self._vars in child classes. Also the update() API is itself quite ugly IMHO. Now you don't need anything fancy to make the whole thing more pythonic - here's a solution whose only boilerplate is the need to call Updateable.__init__ with named arguments (won't work with positional ones):

class Updateable(object):
    def __init__(self, **kwargs):
        self._vars = kwargs

    def update(self, **kwargs):
        vars = self._vars.copy()
        vars.update(**kwargs)
        self.__init__(**vars)


class Rectangle(Updateable):
    def __init__(self, length, perimeter):
        super(Rectangle, self).__init__(length=length, perimeter=perimeter)
        self.length = length
        self.width = 0.5*(perimeter - 2.*length)

r = Rectangle(10, 20)
r.update(perimeter=40)

As a side note, I personnaly find quite disturbing that your Rectangle class takes a perimeter argument but stores a width instead... Maybe you should consider a perimeter property ? (even if read-only to avoid recomputing etc)

like image 31
bruno desthuilliers Avatar answered Dec 14 '25 13:12

bruno desthuilliers



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!