Search code examples
pythonpython-3.xclasscompositionkeyword-argument

Function to set properties of an object of a class composition


I would like construct a class composition that includes a function set_props for setting the instance variables of components. The application for this is in defining new objects for drawing in matplotlib. One example is that I would like to have a function drawMyArrow that draws an arrow with possibly different colors (and other specifications) for its head, tail, and arc. I would like to be able to pass various specifications for the head, tail, and arc via keyword arguments in drawMyArrow. I haven't worked with classes before, but reading up on this online, I believe that the best way to solve my problem is to define a class MyArrow that is a composition of some classes ArrowHead and ArrowArc.

To illustrate my problem, I am using a toy example (that I also used for a previous question here). Let's define a class Room that is a composition of the classes wall, window, and door.

class Door:
    def __init__(self, color='white', height=2.3, width=1.0, locked=True):
        self.color = color
        self.height = height
        self.width = width
        self.locked=locked

class Window:
    def __init__(self, color='white', height=1.0, width=0.8):
        self.color = color
        self.height = height
        self.width = width

class Wall:
    def __init__(self, color='white', height=2.5, width=4.0):
        self.color = color
        self.height = height
        self.width = width

class Room:
    def __init__(self):
        self.door = Door()
        self.window = Window()
        self.wall = Wall()

If I want to have a function for Room that can set properties of its components, I can do something like this:

def set_windowprops(r, color=None, width=None, height=None): 
    if not color==None: r.window.color=color
    if not width==None: r.window.widht=width
    if not height==None: r.window.height=height
    return r

But if I decide to add more instance variables to Window, I would have to go back to this function and add the new instance variables. Can I write set_windowprops so that it automatically accepts all instance variables of Window as keywords?

Ideally, I would like to write a function like this:

def set_props(r, windowcolor=None, windowwidth=None, windowheight=None,
              doorcolor=None, doorwidth=None, doorheight=None, doorlocked=None,
              wallcolor=None, wallwidth=None, wallheight=None): 
    if not windowcolor==None: r.window.color=windowcolor
    if not windowwidth==None: r.window.widht=windowwidth
    if not windowheight==None: r.window.height=windowheight
    if not doorcolor==None: r.door.color=doorcolor
    if not doorwidth==None: r.door.widht=doorwidth
    if not doorheight==None: r.door.height=dooorheight
    if not doorlocked==None: r.door.locked=doorlocked
    if not wallcolor==None: r.wall.color=wallcolor
    if not wallwidth==None: r.wall.widht=wallwidth
    if not wallheight==None: r.wall.height=wallheight
    return r

but without the need of hardcoding all instance variables of components into the function.

I was looking into using keyword dictionaries like so:

window_vars = getNamesOfInstanceVariables(Window) #TODO
window_kwargs = {}
for v in window_vars:
    window_kwargs[v] = None

def set_windowprops(r, **kwargs):
    for kw in kwargs:
        if not kwargs[kw]==None:
            r["window"][kw] = kwargs[kw] #TODO
    return r

Two issues keep me from getting this to work:

(1) In the last line, I am pretending that r is a dictionary (of dictionaries) and using dictionary syntax to assign a value to r.window.kw. But that doesn't work because r is an instance of Room and not a dictionary. What would be the syntax for setting instance variables if the name of the component class and the name of the instance variable are given as strings?

(2) I have tried using inspect to write getNamesOfInstanceVariables, but I am unable to get it to work robustly. Many classes in matplotlib inherit from base classes. I would like getNamesOfInstanceVariables to return all instance variables that a user can set for an object of this class. For example, the class FancyArrow in matplotlib has Patch as base class and instance variables head_length and head_width. So I would getNamesOfInstanceVariables(FancyArrow) to return ['head_length','head_width', *listOfInstanceVarsForPatch].

EDIT

Let me add a bit of background on why I am asking for a dynamical way to write these functions. Let's say I have finished my script and it includes the classes Window, Door, Wall and many class compositions of these three. One of these many class compositions is Room. This class Room has ten hardcoded set_ functions that look like this:

def set_windowcolor(r, color):
    r.window.color = color
    return r

I now decide that I want to add another instance variable to the class Window. For example,

class Window:
    def __init__(self, color='white', height=1.0, width=0.8, open=False):
        self.color = color
        self.height = height
        self.width = width
        self.open = open # new attribute of Window

Similar to all the other instance variables of window, this new attribute of Window should be customizable in all classe compositions that contain a Window. So I would go through my code, find the class Room and add a function

def set_windowopen(r, open):
    r.window.open = open
    return r

I would also have to look for all other class compositions that contain a Window and update them manually as well. I don't want to do this because it is a lot of work and I am likely going to overlook some class dependencies in the process and introduce bugs into my code. I am looking for a solution that either

  • generates set functions for single properties (e.g. set_windowcolor) automatically in Room for all instance variables of Window or

  • automatically adjusts the list of keyword arguments in set_windowprops or set_props.


Solution

  • Here is what I would do

    class Room:
        def __init__(self, kw_door=None, kw_window=None, kw_wall=None):
            if kw_door:
                self.door = Door(**kw_door)
            else:
                self.door = Door()
            if kw_window:
                self.window = Window(**kw_window)
            else:
                self.window = Window()
            if kw_wall:
                self.wall = Wall(**kw_wall)
            else:
                self.wall = Wall()
    

    effectively you are accepting a dictionary that will be unpacked into the instance creation, and when the class definition gets new attributes, they too will be unpacked if they are found in the passed dictionary.