masthead
 
 

Playing Around With Python & Objective C

Using PyObjC to cross the bridge both ways!

3-26-2008

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:

  1. Keep the Python stuff in Python with minimal hacking and editing and modifying of code
  2. Keep the Interface Stuff and associated Objects (aka the "Controller") out of my Python code.
  3. 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). 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:

@class PythonStuff;

@interface NSObject (MethodsThatReallyDoExist)
- (NSArray *) arrayOfNamedStrings;
@end

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.

Then bbum uses another nifty Objective-C call to get the object:

Class PythonStuffClass = NSClassFromString(@"PythonStuff");
stuff = [PythonStuffClass new];

"PythonStuff" is a class he declared in a project file called Pythonstuff.py with a declaration that looks approximately like:


NSObject = objc.lookUpClass(u"NSObject")
class PythonStuff(NSObject):

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. 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!

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. In this case, the Objective-C object called a python method like:

Class PythonStuffClass = NSClassFromString(@"PythonStuff");
myPythonObject = [PythonStuffClass new];

NSString * myString = [myPythonObject dosomethingcool];

This worked and I was excited.

 

But I got stuck when trying to pass other variables across the bridge.

My next attempt was to do the following:

NSString * myString = [myPythonObject PyStringFunction: myNSString];

The goal here was to pass an NSString and get a string back. Python is so good at manipulating text, that this is a no-brainer. And guess what -- it worked!!

So I got cocky, and tried:

int aInteger = 88;
NSString * myString = [myPythonObject PYIntegerFunction: aInteger];

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:

In the Objective-C source:

@class PyObject
@interface NSObject (MethodsThatReallyDoExist)
-(NSString *) returnString;
-(NSString *) PyIntegerPlay:(id)aNumber
@end


...somewhere in the init function....
    Class PyObjectClass = NSClassFromString(@"PyObject");
myPyObject = [PyObjectClass new]; ...somewhere else in Objective-C land.... NSString * temp = [inputNumberField stringValue];
NSNumber * tempint = [[NSNumber alloc] initWithInt:[temp intValue]];
myNewString = [myPyObject PyIntegerPlay:tempint];
In the Python source:
from Foundation import *
import objc									      
NSObject = objc.lookUpClass(u"NSObject")
 										    
class Gamer(NSObject):
    def outputAsBinary_(self, aNumber): 
       print  "The passed type is " + str(type(aNumber))
       x = objc.repythonify(aNumber)
       print "The type(x).__bases__ = " + repr(type(x).__bases__)
       x = y + int(aNumber)
       return "Some string"

The resulting log output shows that the NSNumber is converted to a subtype of 'int'.

The passed type is <class 'objc._pythonify.OC_PythonInt'>

The type(x).__bases__ = (<type 'int'>,)

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.

Where to from here?

So the PyObjC bridge is bi-directional if you read into the documentation and assume a little bit. Strings are easy, the other data types take a little work.

Here's a quick table for those of you still paying attention. I don't guarantee anything other than the fact that I piped these values back and forth and checked their classes and superclasses.

This Class From Objective-C
Yields this Class and __base__ Class in Python
This Class from Python
Yields this Class and SuperClass in Objective-C
NSNumber (as an int) objc._pythonify.OC_PythonInt __base__ = 'int'   int class = NSCFNumber super = NSNumber

NSNumber
(as a float)

objc._pythonify.OC_PythonFloat
__base__ = 'float'
  float class = NSCFNumber super = NSNumber
NSString, NSMutableString objc.pyobjc_unicode
__base__ = 'unicode'
  string ""

class = OC_PythonString
super = NSString

      unicode string u""

class = OC_PythonUnicode
super = NSString

NSMutableArray, NSArray objective-c class NSCFArray
__base__ = objective-c class NSMutableArray
these can be assigned to an array []
  array [] class = OC_PythonArray
super = NSMutableArray
NSMutableDictionary, NSDictionary objective-c class NSCFDictionary
__base__ = objective-c class NSMutableDictionary
these can be assigned to a dict {}
  dictionary {}

class = OC_PythonDictionary
super = NSMutableDictionary

 

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. Eventually I hope to post better working examples...... keep an eye out (on this page) for them.