November 21, 2003

PyObjC

Filed under: ResearchAndDevelopment — Ryan Wilcox @ 5:46 pm

I tried out PyObj-C last night. PyObjC is a language binding/module that lets you use Python with Cocoa – somewhat like how AppleScript Studio lets you use AppleScript to write your Cocoa program.

Except PyObjC makes AppleScript Studio look like Apple took the worst bits of VisualBasic, layed a verbose language on top of it, and called it good.

I’m so very impressed by PyObjC. I used InterfaceBuilder just like I would if I was writing a Cocoa application in Objective-C – defining subclasses, instantiating them, linking them up to actions – and your Python classes are called just as if they had been Objective-C classes.

In Objective-C/PyObj-C Cocoa, interface elements such as windows, menus, etc, live in .nib files. Except nibs don’t just contain interface element definitions – they also contain objects and their connections. Meaning that you can subclass an object (NSObject), create an action, and define things like “when that object over there is triggered, do this method”. These subclasses are “freeze-dried” (similar to pickled objects, if you’re familiar with the Python terms) in the nib file, and created when your application is launched. There are plenty of frameworks that let you create “views” where you define placements of objects, but very very few that let you establish those types of connections.

Applescript Studio takes a very different approach. You drag out your element in your nib, get info on it, name your object, define what script it belongs to, and check which actions that you want to handle. Compared to how Objective-C/PyObjC handles all of this, this method feels very slow (and very different). Part of this probably has to do with AppleScript’s poor object-oriented support.

Back to PyObjC. So, quick as a whistle I drew up a little controller class. Take a look at this:


class MyController(NibClassBuilder.AutoBaseClass, NSApplicationDelegate):
"""
Note that by inheriting from AutoBaseClass, PyObjC will
automatically define MyController based on the definition of
MyController in MainMenu.nib. The inheritance of this class will
be determined by the class definition in the NIB and all outlets
will automatically be defined.
"""
def doAction_(self, sender):
"""
An example of a standard target action method implementation.
"""
hexValue = self.hexValueEdit.stringValue()
myVal = int(hexValue, 16)
self.decValueEdit.setStringValue_(myVal)

This code translates a hex number (typed into an edit field) into a decimal number.

Let’s look at some of the more interested parts of this code:


def doAction_(self, sender):

The sender variable will be an Objective-C class (in this case, an NSTextField). You use this exactly like you would use it in Objective-C – all the functions are Cocoa functions.

In AppleScript Studio, this is not true at all. You get an AppleScript object passed as your parameter, and you have to talk to these in a totally different (but wholly AppleScript) way. In one way, if I’m in Applescript, I want to think in Applescript. In another way, it means that knowing Cocoa doesn’t mean you know AppleScript Studio.


hexValue = self.hexValueEdit.stringValue()

The careful reader says “hey, what’s hexValueEdit?” just about now. It’s an outlet I defined in my nib file – through an outlet I can talk to other elements in the nib. In this case, hexValueEdit is the text field where the user has typed in a hexidecimal number, waiting patiently for it to be translated into decimal format.

Also notice the stringValue() method. stringValue() is an instance function of NSTextField objects in Cocoa. Let me repeat: This is not something provided by PyObjC – it is provided by Cocoa. Stop and think about this – I’m using Python to do this. Cocoa only supports Objective-C and Java (and AppleScript) – but I’m calling Objective-C methods from Python. Amazingly cool.


self.decValueEdit.setStringValue_(myVal)

Again, notice the decValueEdit object. This is the text field where I display the results to the user. Notice the method it calls.

setStringValue_() ?

This is slightly annoying (and it took me a good half hour of fighting before I figured it out), but it’s caused by Objective-C.

See, in Objective-C, the function call would be:


[decValueEdit setStringValue: myVal];

The colon means “a variable comes after this”. There are also more complex calls such as:


[RWError errorWithString: myString going:kToFile to:filepath];

Notice that, unlike C/C++, Objective-C uses labeled parameters separated by colons. Update 12-01-03: Sort of anyway. See the comments by Bill Bumgarner (also) linked to at the bottom of the page

Update 12-01-03: See the RWError page for more about this Objective-C class

In PyObjC you would call the same function like this

RWError.errorWithString_going_to_(myString, kToFile, filepath)

Notice how the labels are now in the function name itself, and separated by underscores instead of colons. (The underscores vs colons issue makes sense – colons are syntax elements in Python.)

Now Python does labelled parameters too, so I don’t understand the reasoning behind having the labels in the function name syntax. Wouldn’t something like the following be closer to what you get with Objective-C?


RWError.errorWithString(myString, going = kToFile, to= filepath)

I don’t know. Maybe they implemented it this way because of speed issues (although a transformation like that shouldn’t take much time).

Yet using Cocoa with Python is simple – if you know how to use Cocoa with Objective-C, you know how to use Cocoa with Python. Very little extra knowledge (other than knowing the language) is required. Compare this to the Using Cocoa The AppleScript Way you get with AppleScript Studio, where you have to deal with another whole API (it seems) to interact with your interface elements.

Last night I created a hexidecimal to decimal number converter application, because it was easy. I’d love to take PyObjC out for a larger project, to see what kind of benefits I would get from the Python vs Objective-C difference. Although they are both dynamically bound (for example, you can call a method in an object, and the compiler doesn’t care if that method exists or not -it may or may not warn you about it, but it won’t give an error), Objective-C has C’s syntax holding it back – semicolons, requirements of prototypes, etc. I would think that with Python, because you have less syntax to worry about, would be much faster to develop in than even Objective-C.

Again, I’m very much impressed at how much of the Cocoa paradigm is preserved by PyObjC. Very good work guys!

Update: 11-29-03: Edited for grammar, and clarified some Cocoa concepts, for those that come here with little background in that area.

Update 11-29-03: Bill Bumgarner explains more about labeled parameters (which, turns out, aren’t technically in either language). I’ve also added a link to my RWError Objective-C class.

9 Comments

  1. One problem with:

    errorWithString(myString, going = kToFile, to= filepath)

    is that -errorWithString:going:to: and -errorWithString:to:going: are different selectors in Objective-C, whereas the order doesn’t matter in Python. Also, you can have selectors like รข??performSelector:withObject:withObject:. Keyword arguments are not the same thing as interspersing the parameters with the name.

    The underscore scheme has some nice consequences. For instance, in Python the value of RWError.errorWithString_going_to_ is a callable object that does exactly what you would expect. It would be messier to get that object using the keyword scheme. It’s nice to have a one-to-one correspondence between methods and selectors.

    Comment by Michael Tsai — November 30, 2003 @ 10:00 am

  2. If your PyObjC is at all recent, you don’t need to inherit from NSApplicationDelegate.

    Also, everyone thinks “why not use keyword arguments?” when they first see PyObjC. Everyone is wrong, but it’s hard to articulate why.

    Comment by Michael Hudson — December 2, 2003 @ 7:40 am

  3. Sounds like PyObj-C could really hit the sweet spot for an awful lot of people. A few sideways thoughts re. Studio and AppleScript:

    “”Part of this probably has to do with AppleScript’s poor object-oriented support.””

    AppleScript’s OOP support – which is prototype-based, not class-based, incidentally – is one of its nicer (less unpleasant?) features imo. Doesn’t have a huge featureset, but all the essentials are there and there’s plenty you can do with it. Rather, I think the reason Studio works as it does is due to the limitations of its target userbase.

    To elaborate: most AS users aren’t trained programmers and just don’t know (or care) about stuff like OOP, abstraction, top-down design, etc, etc. So there’s no way they’re going to think as far ahead as MVC, connections and all the other stuff that goes into professional GUI app design – heck, there’s an awful lot that don’t even grok “basic” concepts like “What are subroutines and why are they used?”(!) Therefore Studio has to use a model that’s accessible to such folk. I guess you could call it a “literal, reactive interface” or something if you wanted to put a label on it. Thus [e.g.] clicking a button fires a generic on-clicked event at the script – i.e. the event describes the literal action rather than its intent – and it’s left to the script to react to this event as it sees fit, figuring out where it came from and behaving appropriately.

    Klunky? Definitely. But a good fit for the existing knowledge and thinking processes of your average AppleScripter, because (1) it takes relatively little abstract thought to grok such a simple and direct cause-effect relationship, and (2) it gives them an object, which is something they already know how to manipulate, that they can beat and hammer at with nothing more than the basic control structures (i.e. conditionals and loops) until they get it to do what they want it to.

    To be honest, I don’t like Studio either, but that’s because I think its users would have been much better served by a HyperCard-style environment where there’s even less separation between GUI and code, so even less abstract design and thought required. I guess like anything Apple does these days it’s more a practical compromise than an ideal solution to the problem: long on pragmatism; bit short on vision. (But hey, you pays your money…;)

    Comment by has — December 2, 2003 @ 10:00 am

  4. Has has some interesting comments about Studio. The ‘the-technology-is-like-that-because-of-the-users’ argument is a good take on the situation. Calling an arbitrary handler in some Applescript to deal with an action should be just as easy as calling an “on clicked” method, programmically speaking.

    For me, Applescript makes an OK (but verbose) glue language. Studio also allows you to use Objective-C/Cocoa concepts when it’s useful (and I’ve done it before – see http://www.wilcoxd.com/blog/archives/000017.html ). That part is pretty cool, to tell the truth.

    I hesitated to write that line about poor object oriented support. Because, in a way, it’s great. You talk to an application (an object) and get its frontmost state (a property). Whose statements are great (where they’re supported). A programmer can make their own modules/script objects, with state (properties) as well.

    I guess my experiences with Applescript Studio (currently finishing up an application that’s about 3500 lines of Applescript, and 300 lines of Objective-C) are much different from the normal Applescripter who wants a window with a progress bar for their Quark solution.

    Comment by Ryan Wilcox — December 2, 2003 @ 11:10 am

  5. > Michael Hudson said:
    >
    > “If your PyObjC is at all recent, you don’t need
    > to inherit from NSApplicationDelegate.”
    >
    > This makes sense – I probably only have to do that
    > for delegate objects, which this isn’t. (or is it
    > totally obsolete now?)

    I hadn’t twigged that this wasn’t a delegate object. Inheriting from NSApplicationDelegate when instances of the class aren’t going to be application delegates is just silly :-)

    The point was that you don’t need to inherit from NSApplicationDelegate ever any more.

    Comment by Michael Hudson — December 3, 2003 @ 6:21 am

  6. “””Calling an arbitrary handler in some Applescript to deal with an action should be just as easy as calling an “on clicked” method, programmically speaking.”””

    Absolutely. And I think this approach would’ve made Studio much nicer for folk who have a bit of programming experience, but – crucially – at the expense of those who don’t. So I guess I’m glad for Studio users – if not myself – that they didn’t. (And I have to say, something I dislike about IB’s Connections system is it somehow seems to make things look rather more complicated and intimidating than they actually are. Though I can’t quite put my finger on why I feel like this.)

    “””For me, Applescript makes an OK (but verbose) glue language.”””

    AppleScript’s “verbosity” is actually a feature, and a key one at that. Again, you have to consider it in context of its core market, i.e. non-programmers who just want to get something done. For them, a verbose keyword-oriented syntax is far easier to read than a terse, symbol-laden syntax. It enables non-programmers to grok what a script does just by reading the thing – even if they’re not yet familiar with the AppleScript language itself! This is incredibly important to them as it lets them get that first foot in the door with almost zero effort. [1]

    That said, there’s no shortage of other areas in which AppleScript does genuinely suck. But let’s not spend the next week going there…;)

    “””I hesitated to write that line about poor object oriented support. Because, in a way, it’s great. You talk to an application (an object) and get its frontmost state (a property). Whose statements are great (where they’re supported).”””

    Actually, none of the above are features of the AppleScript language itself. They’re all features of the Mac’s high-level interapplication communication mechanism as provided by the AppleEvent Manager, which AppleScript hooks into via the Open Scripting Architecture. (This is an incredibly common misconception, btw, so don’t feel bad about it.;)

    Basically, any language that can hook into the OS can do application scripting [2]; it just so happens that AppleScript is the most well-known – as well as having what is probably the best and [with some caveats] easiest to use implementation.

    One of the interesting things about the AEM is that the application scripting interface works more like a query language than a conventional object model [3]. It just so happens that AppleScript (and, I suppose, others) happen to dress this mechanism up in different clothes. It could just as easily have been done by having embedded SQL-style statements,

    “””A programmer can make their own modules/script objects, with state (properties) as well.”””

    Yup, and it’s great fun for prototype-based OOP. Terrifically liberating. Even if it does feel a bit like running down the public high street with no pants on.:)

    [1] Just as C’s extremely terse syntax is a key feature for professional programmers who want to avoid getting the dreaded “Cobol fingers”. :)

    [2] Frankly, I think it’s a crying shame that Apple hasn’t done more to push AppleEvents/Open Scripting Architecture support for other languages such as Perl, Python, Ruby, etc. While the actual implementation is less than perfect (IMO), the general concept is absolutely brilliant and doesn’t deserve to be hogged by just a single minor language (especially not one as flawed as AppleScript).

    [3] Unless you’re talking about one of the more exotic OO implementations that also have array proccessing-style features built in (e.g. F-Script). The AppleScript language isn’t one of these, however.

    Comment by has — December 3, 2003 @ 7:50 am

  7. “”Frankly, I think it’s a crying shame that Apple hasn’t done more to push AppleEvents/Open Scripting Architecture support for other languages such as Perl, Python, Ruby, etc””

    I played with a Python OSA component by Bill Fancher a few years back (2001). It was really cool being able to write Python in the Script Editor.

    This was back in the pre 10.1 days – so there wasn’t even Python on machines by default then. It was certainly be an interesting thing to resurrect now, as we have a decent install of 2.3 by default on Panther.

    But yes, it’s very sad we have so few OSA languages. Applescript, and Javascript – that’s it (not including Bill’s Python one, because well, it’s not out).

    Comment by Ryan Wilcox — December 3, 2003 @ 9:51 am

  8. Objective C’s (or Smalltalk’s) keywords seem more novel than they are. Internally each keyword invokation is compressed into a single string/symbol and the arguments are a simple tuple. So this:

    [RWError errorWithString: myString going:kToFile to:filepath];

    Becomes:

    RWError.errorWithString:going:to:(myString, kToFile, filepath)

    This isn’t just a PyObjC transformation, but the actual transformation underlying Objective C. You’ll see experienced programmers refer to methods this way (i.e., simple as errorWithString:going:to:), and some of the introspective techniques (at least in Smalltalk) involve creating these method symbols combined with a tuple of arguments. So PyObjC is really doing the minimal work, providing a one-to-one mapping from Objective C to Python (with _’s).

    It would be interesting if PyObjC used heuristics to figure out how to parse keyword arguments, essentially testing all possible combinations, then saved that mapping, dynamically creating functions like:

    def errorWithString(self, string, going, to):
      self.errorWithString_going_to_(string, going, to)

    going and to can’t magically attain default arguments, though if there are also methods like errorWithString:, or errorWithString:to:, then you might have to grow the method over time, ending up with something like:

    def errorWithString(self, string, going=NoDefault, to=NoDefault):
      if going is NoDefault and to is NoDefault:
        return self.errorWithString_(string)
      elif going is NoDefault:
        return self.errorWithString_to_(string, to)
      elif to is NoDefault:
        assert 0, “errorWithString:going: does not exist”
      else:
        return self.errorWithString_going_to_(string, going, to)

    Or maybe instead of that assert, we recreate the method, trying to find errorWithString_going_; we could also add a **kw argument, and rebuild the method if unknown keywords are passed. I don’t know how doable this all is, or what the performance change would be (maybe not too bad, so long as the keyword to method mappings were cached)… anyway, it might be a fun little experiment, and can probably be done mostly in Python, so long as some Objective C introspection is already available.

    Comment by Ian Bicking — December 9, 2003 @ 12:58 pm

  9. Ian,

    Such an experiment was already done. See the Java-ObjC bridge. While the heuristic is not automatic– there is a manual mapping to ensure consistency– the end result is the same in that the ObjC APIs are mapped to more “Java-esque” APIs.

    It sucks.

    The end result is an API that is different enough from the original API that the developer now has the exciting task of having to learn an all new API.

    It also lacks consistency. There will always be cases where the mapping rules had to be bent just slightly because of some method naming conflict or API issue.

    As a result of that need to manually “bend” the mapping, it yields a bridge that requires lots of tedious “detail” maintenance over time. It also guarantees a significant lag between changes to the underlying layer and updates to the bridge.

    There is also a huge hit to dispatch performance. It requires algorithmic string manipulation, including concatenation, comparisons and lookups, before an attempt to dispatch can be made.

    In the end, a bridge that perpetuates a very simple and completely consistent mapping rule — substitute ‘_’ for ‘:’, you’re done — is far easier to maintain and will track releases of the mapped API much more closely.

    Comment by Bill Bumgarner — December 11, 2003 @ 2:50 pm

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.