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:
- 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). 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.
|