Skip to content

The ‘about proxy object project’ in Python Koans

18 September, 2012

I have recently decided to refresh (and update) my python skills. I had looked into Ruby Koans once and really liked the concept. So when I found out that there was a port of the koans code to Python, I figured that would be a good starting point for me. You can get the Python Koans code from github.

Anyway, the post is about a particular koan that stumped me. It is the last exercise in the koan series (second-last if you consider the extra credit koan) and the exercise file is titled ‘about_proxy_object_project.py. The goal of the koan is to develop a class that makes proxy objects using a given parent class and intercepts calls made to the parent class as need be. I tried the problem myself and could not get very far. Naturally, I googled for an answer and found fragments of code that I could use in my solution. However, I did not find a complete answer to that exercise. So I am documenting the solution over here for all of you guys who are stuck in koan hell.

Update: The following error was corrected , so if you have downloaded the koans before the date this post was published, you might have to perform the following steps. Otherwise, just skip the next paragraph.

First things first. I am using the ‘python2’ area of the koans (Python 2.7.3). There is a ‘cosmetic’ error in the current edition of the python koan code in that exercise that needs to be corrected. I will update this post if this gets fixed eventually. For now, before you start the exercise, change the assertions that test for the ‘channel’ property so that they check for the string ‘channel’ rather than ‘channel=’
For eg. self.assertEqual(['power', 'channel='], tv.messages()) would become self.assertEqual(['power', 'channel'], tv.messages())
You could use the exercise without making the change above, but that will just result in string manipulation on your part to get the right strings for class properties.

It is always better that you try to solve it on your own, but I needed help from the interwebs so you might as well. That being said, here is how you do it. The basic idea is to intercept the way methods and properties are called in Python and manipulate them if necessary or pass them on to the parent object. To do that we redo the __getattr__ and __setattr__ methods in our proxy class and ensure two things happen. If the attribute that was called is of interest to the proxy class, the class handles it and returns values accordingly. If not, then it return the getattr or setattr methods on the object rather than self using __getattribute__ . Refer to the code below. The rest of the code is like any python code except for another tiny difference. When defining variables for the proxy class, instead of using the familiar self._messages format, we have to use the setattr method. The reason for this is that when setting these variables the normal way, the __setattr__ method is called by default on the instance attribute. To circumvent this behaviour and call it on the base class method, we have to use object.__setattr__(self, '_messages', []) as shown below. My solution juggles the base and instance methods to get the job done and it may be a hack (I don’t know for sure). So if any Pythonista has a better way at implementing this koan, do respond in comments. The entire class code is given below. If you copy paste the code make sure your indents are right. 
class Proxy(object):
    def __init__(self, target_object):
        object.__setattr__(self, '_messages', [])
        object.__setattr__(self, '_was_called', {})
        #initialize '_obj' attribute last. Trust me on this!
        object.__setattr__(self, '_obj', target_object)

    def messages(self):
        return self._messages

    def was_called(self, attribute):
        return attribute in self._was_called

    def number_of_times_called(self, attribute):
        if attribute in self._was_called:
            return self._was_called[attribute]
        else:
            return 0

    def __setattr__(self, attr_name, value):
        self._messages.append(attr_name)
        if attr_name in self._was_called:
            self._was_called[attr_name] += 1
        else:
            self._was_called[attr_name] = 1
        setattr(object.__getattribute__(self, "_obj"), attr_name, value)

    def __getattr__(self, attr_name):
        if attr_name == 'messages':
            return self.messages()
        else:
            self._messages.append(attr_name)
            if attr_name in self._was_called:
                self._was_called[attr_name] += 1
            else:
                self._was_called[attr_name] = 1
            return getattr(object.__getattribute__(self, "_obj"), attr_name)

Advertisements
6 Comments
  1. Thanks, helped me understand the lesson. regards from Venezuela o/

  2. Vit permalink

    What do you think about my solution:

    class Proxy(object):
    def __init__(self, target_object):
    object.__setattr__(self, ‘_obj’, target_object)
    object.__setattr__(self, ‘_messages’, [])

    def __getattr__(self, item):
    self._messages.append(item)
    return getattr(self._obj, item)

    def __setattr__(self, key, value):
    self._messages.append(key)
    setattr(self._obj, key, value)

    def messages(self):
    return self._messages

    def was_called(self, attr_name):
    return attr_name in self._messages

    def number_of_times_called(self, attr_name):
    return self._messages.count(attr_name)

    • Vit permalink

      Sorry for bad editting

      • akshaydandekar permalink

        Your code is definitely simpler and cleaner than mine. Bravo! I was having issues using a list at the time, so I opted for a dict. So if lists worked for you, that’s awesome!

  3. Vit, did this work? I had a similar solution that fails because it’s getting an extra __setattr__() for ‘_obj’ at the beginning of my _messages[]. I borrowed your list.count() piece–nice touch, eliminates some of the ugliness I’ve seen with other message counting solutions.

    Still stuck:

    test_proxy_records_messages_sent_to_tv has damaged your karma.

    You have not yet reached enlightenment …
    AssertionError: Lists differ: [‘power’, ‘channel’] != [‘_obj’, ‘power’, ‘channel’]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: