masthead
 
 

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:

  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, 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)

//
// MyPythonFun.m
// Red Byer

#import "MyController.h"

@class PythonStuff;

@interface NSObject (MethodsThatReallyDoExist)
- (NSArray *) pyNamedStrings;
- (NSString *) pyGetAString;
- (NSNumber *) pyGetANumber;
- (void) pySetANumber:(NSNumber *)aNum;
- (void) pySetAString:(NSString *)aString;
- (void) pyDoSomethingElse;
@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. 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:


// MyController.h
// MYPYTHONSTUFF

#import <Cocoa/Cocoa.h>
#import <Python/Python.h>

@interface MyController : NSObject {
    //------PYTHON BITS----------
    IBOutlet id inputStringField;
    IBOutlet id inputNumberField;

    NSObject * myPythonStuff;
    NSString * tempString;

}

- (IBAction)setValues:(id)sender;

 

Then bbum uses another nifty Objective-C call to get the object (6-21-2010 Update with more info, sorry about the lack of indentation):

- (id)init
{
    self = [super init];
    Class PythonStuffClass = NSClassFromString(@"PythonStuff");
    myPythonStuff = [PythonStuffClass new];
    //Other setup and initialization stuff for Objective-C
    return self;
}

"PythonStuff" is a class he declared in a project file called Pythonstuff.py with a declaration that looks approximately like (6-21-2010: Updated with even more detail):

#MY PYTHON STUFF FILE
#pythonstuff.py

#standard Python imports
import string

#pyObjCImports
from Foundation import *
import objc

NSObject = objc.lookUpClass(u"NSObject")

#for debugging
import sys
print "PYTHON VERSION BEING USED:"
print sys.version


NSObject = objc.lookUpClass(u"NSObject")

#NOTE: For now we will standardize so that any function available across the Objective-C
#bridge begins with a "py" (similar to how "_" precedes so-called private methods)

class PythonStuff(NSObject):

def new(self):
    #new() seems equiv to .alloc().init() for PyObjC
    """
    Designated initializer for PythonStuff. This is a KEY part in
    getting the pyobjc bridge to work!!! We actually call the standard
    __init__ function to keep things Pythonized.
    """
    self = super(PythonStuff, self).new()
    print u"new PythonStuff created" #for debugging
    if self is None:
        return None
    else:
        self.__init__()
    # Unlike Python's __init__,objc initializers MUST return self,
    # because they are allowed to return any object!
    return self

def __init__(self):
    self.MyString = "" #gets cleared/recereated every run
    self.MyNumber = [] #gets cleared/recreated every run
    self.FILEPATH = "/Users/red/Desktop/mylist.txt"
    self.TRREEPATH = "/Users/red/Desktop/mytree.txt"
    return None

#----SETTER FUNCTION EXAMPLE-----------------------------------
# those accessible by objc and getting a variable must end in "_"
#-------------------------------------------------------
def pySetInputPath_(self, aString):
    '''sets up the path to the input file with no checks'''
    if aString == None:
        pass
    else:
        self.FILEPATH = aString
    return
def pySetAString_(self, aString):
    #debugging
    #self._whatisit(aString)
    if aString == None:
        self.MyString = ""
    else:
        self.MyString = aString
    return

def pySetANumber_(self, aNumber):
    #self._whatisit(aNumber)
    try:
        self.MyNumber = int(aNumber)
    except:
        self.MyNumber = 99999 #arbitrary NaN type value
    return

#---------GETTERS----------------
#---------NOTE that for functions with only (self) no underscore "_" is required
def pyGetANumbert(self):
    return self.MyNumber
def pyGetAString(self):
    return self.MyString

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)


# main.py
# MYPYTHONSTUFF
#

#import modules required by application
import objc
import Foundation
import AppKit

from PyObjCTools import AppHelper

#######IT IS KEY TO IMPORT ALL THE PYTHON FILES NOW TO ALLOW
#######OBJECTIVE-C TO SUBCLASS PYTHON CLASSES!!!!!!!!!!!!!

# import modules containing classes required to start application and load MainMenu.nib
import BOMTree
import BOM2XGMLAppDelegate

# pass control to AppKit
AppHelper.runEventLoop()

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!

    [inputString setStringValue:[myPythonStuff pyGetAString]];
    [inputNumber setIntValue:[[myPythongStuff pyGetANumber] intValue]];
}

- (void) setSomeValues
{
    [myPythonStuff pySetAString:[inputStringField stringValue]];
    [myBOMExporter pySetANumber:[NSNumber numberWithInt:[inputNumberField intValue]]];
}

Of course, your mileage may vary. Massaging the interface builder fields into the right format to pass to the Python class took a bit. However, this worked and I was excited. return self;

But early on I found that I could get stuck when trying to pass other variables across the bridge.

My next attempt was to do the following:

NSString * myString = [myPythonStuff 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.

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:

def _whatisit(self, item):
    print "THE ITEM IS: "
    print item
    x = objc.repythonify(item)
    print "The passed type is " + str(type(item)) #type() == x.__class__
    print "When repythonify'd, we get: " + str(type(x))
    print "The type(x).__bases__ = " + repr(type(x).__bases__)
    #n = aNumber.intValue() #This is unused, but pyobjc converts ints into this form!
    return None

Then, include a call to this function inside every one of your python setter functions before your try-finally block. You can always comment it out. E.g:

def pySetIgnorePrefixList_(self, aList):
    #self._whatisit(aList)
    try:
        self.IGNOREPREFIXLIST = aList #SOME PROCESSING REQUIRED
        #print "LIST length is " + str(len(aList)) + " & item 0 is " + str(aList[0])
    except:
        self.IGNOREPREFIXLIST = ['15']
        #self._whatisit(aList)
    return

 

 

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

 

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.