Playing Around With Python & Objective C

Using PyObjC to cross the bridge both ways!

Updated 6/21/2010

Intro

I've been playing around with Python for quite some time (3 years for a hobby is quite some time for me). Let's say that I'm happy enough with my ability to hack around in Python. I find the language easy to use, powerful and simple to maintain -- all things that are important when you only touch it once every 3 months or so.

The one thing I've been lacking, however, is a good way to put a GUI front end on some of my little command-line apps. I'm not looking for broad distribution, mind you, so giving it a OS X front end using XCode is just fine (yup, looked at Tkinter and WXWidgets -- not too interested). I've toyed with py2app before and found it a little clunky. I've also touched on PyObjC a little bit, but not enough to really make headway.

Suffice to say that another side project I've had, but never fulfilled, was to learn a little Objective-C (enough to be dangerous) and become familiar with XCode and Interface Builder. Now that I have a snazzy Leopard install on my red 17" MacBookPro, I figured it was a good time to play. So, just recently I dusted off an old book I bough "Learing Cocoa With Objective C" (O'Reilly) and went through all the examples. (OUCH-- Interface Builder 3 is different enough that I struggled with the examples over and over..... but eventually made it through).

So there I was -- Python apps in hand and ready to throw some GUI front ends on things. My goals were simple:

Keep the Python stuff in Python with minimal hacking and editing and modifying of code

Keep the Interface Stuff and associated Objects (aka the "Controller") out of my Python code.

Do the above with a minimal amount of hassle, custom installs and all the stuff that leads one down into a spiral of apt-gets and port installs.

Getting PyObjC going: Turns out that PyObjC is installed with Developer Tools on OS X (10.5 Leopard, 10.6 Snow Leopard)). and it should have been a piece of cake. However, I had a legacy install mucking things up and I had to go in and excise items out of my Python sys.path (it was pointing to the wrong Python.framework, etc etc). Once I cleaned that stuff up, I was able to get the basic demos to build without a problem.

How to make the PyObjC bridge work both ways - with examples!

Because I wanted to have an Objective-C controller class and a Python based model class, I needed a bridge that would go both ways. My Objective-C controller needed to be able to instantiate a Python class and then call instance methods.

The problem is that the documentation for PyObjC is severely lacking in this one area at this time. Web-searches and hours of hunting around, and all the I could find were examples based solely in Python! That's kind of like asking a snake to climb a tree! The PyObjC guys are really good at porting Objective-C entirely over to Python, but they don't show any mixed-use examples that I could find. The documents reference that the bridge works both ways, and even Apple's Developer site says that the bridge works both ways, but almost nobody could show me a solid code example of a bidirectional bridge.

My luck changed when I stumbled across this tiny little example on bbum's weblog. (You can download the source code from his site). He shows how to mix Python, Ruby and Objective-C in one little App. Talk about making snakes slither, gems glow and Objective-C do whatever it does!

I then spent a day figuring out what bbum was up to. You see, he starts by forward declaring the Python class and then adding a category to NSObject (6-21-2010 Update: More detail now included in below example)

This neat trick avoids all the stuff I found about having to precompile the Python and use it as a plugin (annoyance!). The compiler just trusts you know what your doing. NOTE: I also learned early on to clearly distinguish my Python function calls from my internal Objective-C calls by using a little Hungarian Notation prefix "py".

(Update 6-21-2010): Below is a snippet from the MyController.h file to show you an example for completeness sake:

NOTE #1: I find the "new" function an essential handoff function in this case. Perhaps this is anectdotal, but I was having some troubles initializing without this function. You may find you don't need the "new".

NOTE #2: Python methods accessible by objc and getting a variable require a trailing underscore "_". While confusing, methods that only have the (self) variable do not need an underscore ("_"). Look at the setter versus the getter examples above. This has something to do with the way Objective-C and Python hand things off to one-another.

Of special note, however is making sure that the Python files are included before the application is set to load (since Objective-C is runtime-like). In other words, be sure to look at your main.py file and import myPythonCoolness. (SEE BELOW) If you start from one of the XCode templates (Python-Cocoa app, for instance), you need to put the import myPythonCoolness before the following line: AppHelper.runEventLoop(). I had failed to do this and it threw me for quite a loop -- the compiler compiled and the code seemed to run, but nothing was happening! (6-21-2010 UPDATE BELOW)

So I got a basic example working (much like bbum's) with an input text field and an output text field. The interface was controlled by an Objective-C object that instantiated a Python class to do the dirty work. (6-21-2010 UPDATE) Here are some example Objective-C calls to python methods in the MyController.m (assuming we already initialized myPythonStuff as noted earlier:

- (void)awakeFromNib
{
// Right now, we're storing all the values in the python class.
// This might not be the best idea, but with setters/getters it works!

And the bridge collapsed! Seriously...... It seemed like a no brainer passing a basic C-type over the bridge to a python function. Seemed straightforward. Should just work. But, alas, the debugger kicks in and the program halts without so much as entering the first line of Python code. I simply could not figure out what was going on. Why wasn't it working?

To make a day-long story short, I don't necessarily understand what was going on, but I now know how to fix it. It took lots of trials and many errors but I learned a few things that I will now share with you.

The PyObjC bridge does indeed work both ways, but the documentation needs to be improved.

If you go to this PyObjc Page there is a section: Accessing Python Objects From Objective-C that states the possibility but doesn't provide too much of an example or understanding for those of us new to the game. Particularly, look at the segment:

Python numbers (int, float, long) are translated into NSNumber instances. Their identity is not preserved across the bridge.

I don't know about you, but this is almost meaningless to the problem I was facing. This is describing the bridge from Python -> Objective-C but not the other way.

The answer: PyObjC turns an Objective C NSNumber into a Python int.

The following code will pass an int and produce the results seen below:

The pythonification is not necessary to work the with number, but I use the int() call anyway just to make sure things work to some degree (so as not to generate an exception). As far as I can tell, you can work with the int as you would a normal Python int.

Here's Some Serious Help In Debugging The Python Side of the Bridge

(NEW section, 6-21-2010)

Getting some feedback from Python proved essential in mapping out the results of the bridge. I would highly recommend creating yourself a basic "What Is It?" function, e.g:

Wrapping it up

Hopefully this is enough to help some of you out there struggling with the same sort of thing I was. It took me a couple of days of searching and trial and error to figure this all out-- so if you came across this little post I hope to save you some time.

I have also found that sourceforge pyobjc mailing list to be a handy place to ask questions and discuss with other users out there.

Also, special thanks to Billy L. for reminding me to post better examples and dust off this page after a long hiatus.