Binding Class Attributes In Python (Part 2)
Hello everyone, sorry I haven’t blogged for a while but I was busy with other matters and barely managed to allocate enough time over the weeks to write this article. My last article was about how you can use Python to bind to class attributes together so that updating one automatically updates the other. I also demonstrated how we used this method in a college project to implement a MIPS processor by defining each block, defining its inputs and outputs (in terms of inputs) and then, using the demonstrated method, tell Python that the output of this block is connected to the input of the other block and just letting everything evaluate itself automatically. This was, by far, the most viewed article on my blog, so if you haven’t read this article, I recommend you go do that now, just follow this link. The old article also contains a lot of essential background which is required for understanding this article, so if you want to make the most of this article, read the old one first or just google the topics you do not understand when they come up ^_^ .
You might be wondering, if we already have a method for binding class attributes together, why do we need this new method. Here are my top reasons for researching this alternative method.
- The old method
requires classes to be defined in a certain way, it was not a general case
- The old method
was one way only. This means that updating instance 1 updates 2, but you cannot
update 2 and have it automatically update 1.
- Once the class
attributes were binded, they cannot be unbinded.
- It is always super AWESOME and fun to think of new, and better solutions to problems :D.
Let us start by defining a simple class, we will call it ValueContainer.
Simple enough, no? If you’re wondering what will we be using this for, then congratulations on having a brain :D but I guess you’ll have to find out shortly. Next, we will define a new class that we will be using as a descriptor. If you do not know what descriptors are, feel free to google them now. The most popular descriptor you have used is probably the property decorator but we will be defining our own descriptor now. Let’s us start simple and call this descriptor BindingDescriptor.
Okay, so this BindingDescriptor will be initialised and will be given the name of the attribute it is controlling access to. It will also initialise 2 dictionaries. One called getters and the other called setters. The intention is that getters will have instances as their keys and functions as their values. Whenever the function corresponding to a certain instance is called, the value that is “binded” will be returned. Setters has the same mapping except that the function will take an argument that will be set as the “binded” value. We will also define a method called add_instance() that we will use to insert more instances with their getter and setter functions into the getters and setters dictionaries.
Let’s put aside BindingDescriptor for now and move on to our third and final Class. Before we give it a name, You must know that I am a Communications and Electronics Engineering college student, so all the names I give are 100% biased towards my study. Therefore, I will name this class Wire, and I will add a method inside called solder(). This method, should be called with the arguments instance1, instance2, attrname1, attrname2, and to start off simple, we will assume (temporarily) that the instances that will be binded (or soldered) together have the same parent class and the attrnames that are being binded are exactly the same. We will also make the assumption that both instances have not been binded to anything previously. Here is the definition of Wire.solder() with the assumptions we mentioned before.
If you do not know what is the @staticmethod, getattr(), or setattr(), feel free to google.
Now the final piece of the puzzle (assuming our assumptions are correct), defining the __get__() and __set__() of the BindingDescriptor.
And now, let us test to see if it works (with our assumptions in effect)
It works!! (mostly). Remember the assumptions we made earlier? Lots of problems happen if they are not upheld. Moreover, tons of other problems can happen for various other reasons. Therefore, in the next section I will be discussing all these problems and their solutions. A gentle warning though, things are going to start getting messy.
Problems and their solutions
Instances will never get deleted from memory
If you’ve worked with a language like C before, you have probably dealt with the pain of cleaning up the memory manually whenever something is not useful anymore. Thankfully, pythonistas never have to deal with this; thanks to the garbage collector. The garbage collector will remove something from memory when there are no more references to it. Remember when we used the instances as keys for our getters and setters dictionaries? Using them as keys will be considered as referencing them and therefore they will NEVER get deleted from memory because there will always exist a reference to them inside the getters/setters dictionary. To solve this, we will replace the standard dictionary with a WeakKeyDictionary from the weakref module. This special dictionary will allow us to use instances as keys without increasing their reference counter and we can still deal with it as a normal dictionary (with special considerations that are not required here). Here is our updated __init__() from the BindingDescriptor class.
Using the WeakKeyDictionary will allow instances to get garbage collected as soon as all non-weak references of it get removed.
Accessing attributes on instances that are not binded
Remember the 2 final steps of the Wire.solder() method? We set the descriptor on the class of the instance and not the instance. Google will be happy to tell you why, but long story short, that’s how descriptors work. The problem is, under the same assumptions, if we try to access a third instance that is not binded (but the other 2 are) this happens.
See that KeyError exception? It’s because, after soldering a and b, the attribute name “testvalue” on all TestClass instances is now accessed via our BindingDescriptor. This descriptor’s __get__() method expects an instance to have a getter function, but since the c instance was never soldered, it does not have a getter key in this dictionary and therefore will raise a KeyError Exception. Let us fix this by using the __dict__ of the instance to return the testvalue of c. Let us also do the same on the __set__() method of the BindingDescriptor. Here are the updated __get__() and __set__()
Let us test and see if it works.
Great! It does, now let us move on to the next problem.
Soldering a third instance desolders one of the other 2 instances
In this problem, We will remove the assumption that all instances are initially soldered. Maybe you soldered b to a, and now you wish to solder c to a. Will this work with our current implementation?
As you can see, soldering c to b, desoldered a from the both of them. This is because we defined new getter and setter functions for this binding. These new getter and setter functions update a different ValueContainer instance, not the one we used in the earlier binding. What we should have done, is recycle the old getter and setter functions instead of using new ones so that all bindings reference the same ValueContainer object. Moreover, we created a new BindingDescriptor that overwrote the old BindingDescriptor; we should have also recycled the old BindingDescriptor. Solving this problem is a 3 step process. First of all, let us define 2 new methods on the BindingDescriptor
We will use retrieve_getter() and retrieve_setter() to retrieve an old getter or setter if it exists from the getters/setters dictionaries (note: accessing the dictionary using the .get() method will return None if the instance is not found in the dictionary).
Otherwise, we will just re-use the BindingDescriptor, getter, and setter. To implement this more cleanly, I moved getter and setter creation to a new method under Wire called request_new_getter_and_setter(). To be able to call this method, I changed the Wire.solder to a classmethod instead of a staticmethod. Here are the changes I made.
On the 3rd line in the Wire.solder() method we attempt to get the old BindingDescriptor. If the descriptor does not exist, then it will raise an AttributeError exception. We handled this by saying that the potential melt is None, but what if the descriptor existed? The problem is, because attempting to access the attribute (attrname1) on the parent class is done through the descriptor itself, the descriptor must be told to return itself if the attribute was being accessed on None instance (means the access is being done on the parent class itself). Let us fix this. Here is the updated __get__() method on the BindingDescriptor
Now let us test and see if it works
Huzzah!!!, It works, let us move to the next problem.
Binding instances of different classes, or same classes but different attrnames or different classes and different attrnames
This is by far the most annoying of all problems and to tackle it we will need to get rid of all of our assumptions that we made earlier and start thinking of how we will deal with all the different cases. To make this easier, we will first define a check called completeMatch. completeMatch will be True when both instances are of the same parent class and attrnames match. We will test for completeMatch at the very beginning of the Wire.solder() code.
Since we got rid of all our assumptions, we now have a complete mess. Here are some cases that we can encounter.
- Both instance/attrname combinations might have not been binded before, and therefore will not have a BindingDescriptor, getter, and setter.
- One of the
instance/attrname combinations might have a BindingDescriptor, getter, and
setter functions but the other does not have these.
- One or both of the instance/attrname combinations might have a BindingDescriptor but no getter or setter functions; This might happen if another instance from the same parent class was binded before on the same attrname.
To clean things up, we will first define a method on Wire called retrieve_getter_and_setter(). This method will try to retrieve the getter and setter from the BindingDescriptor of instance1, if it fails, it will try to get them from the BindingDescriptor of instance2, if it fails, it will just request_new_getter_and_setter() and return them.
Now let us use our newly created method to attain getters and setters.
Note: The getter and setter functions will always be the same for both classes, this is the main concept that makes this idea work in the first place.
The updated solder method will now work as follows.
Okay so let us test this against lots of ciritcal cases and normal cases.
And here are the test results.
Once instance attributes are soldered, they cannot be desoldered.
This problem is an easy pick, because desoldering an instance is as easy as removing its getter and setter functions from the respective descriptor. So let’s define define a Wire.desolder() method on the Wire class. The Wire.desolder() method should be given the instance, the attribute name and an optional argument of whether we want to maintain the soldered value, or restored to the value this instance had before being desoldered. Here is the definition I wrote for the desolder method.
Now we need to go ahead and define the remove instance method on the BindingDescriptor. So here is how we will do this.
And that’s it!! First we will retrieve the original value using the getter of the instance in case we need it. We will then proceed to delete the getter and setter function of the instance. Afterwards, if the user wishes to maintain the soldered value on the desoldered instance, we will update the instance’s __dict__ with the value that was previously soldered on this instance.
If the BindingDescriptor did not exist on the class, trying to getattr it in the Wire.desolder() method would have resulted in an AttributeError Exception. If the BindingDescriptor existed but did not have this instance as a key in either its getters or setters dictionary, this will raise a KeyError exception. We will catch both exceptions in the Wire.desolder() method we defined earlier and ignore them.
Let us try and see if this works. Here are the tests and the test results.
After reading part 1 of this article and this part, you might be wondering which one is the faster method? I performed quick tests to get an overview of which method would be faster in a real life application and found this new method to perform consistently faster.
Being able to bind or solder python class attributes together opens up an entire range of possibilities. Imagine being able to get rid of callback functions that do nothing except update values. But before you get too excited, this is Python and not magic. Instead of updating the variables yourself, you’re letting Python do it for you. So do not expect this to have any less update time for the variables you are trying to access.
Please tell me what you think of this article, what do you want to see me write about, and if you have any question or feedback leave it in the comments section below and I will respond as soon as I am able.