PyObjC learnings

July 26, 2007

Lesson #1: Don’t forget to return self when you override init. Otherwise nothing works, but it isn’t obvious why.

Lesson #2: Initialize instance variables in init. I started out trying to avoid overriding init whenever possible. Perhaps it was because I was gunshy after learning Lesson #1 the hard way. Also, I don’t like the two lines of boilerplate that begin every overridden init:

class MyObject(NSObject):
    def init(self):
        self = super(MyObject, self).init()
        if self is None: return None

One of the great things about Python is its terseness, and having to type that stuff every time is annoying. I think that if I knew enough Python I could write a function decorator to handle the obligatory call to superclass, but then I’d have to add the decorator module to the project and import it — not a big savings in typing.

So when I needed to initialize an instance variable I tried to avoid overriding init, and instead would perform the initialization in the class definition:

class MyObject(NSObject):
    _array = []
    _lastIndex = 0
   ...etc...

That way instance methods could refer to self.instance-variable-name and get the initialized class attribute if no per-instance value had yet been set. Having that series of assignments at the top of the class definition also served to gather together all the instance variable names I planned to use, rather than having them scattered through the code.

But experienced Python programmers will immediately spot the problem. Objects assigned to class attributes are shared between all the instances of the class, and so changes made to mutable objects by one instance are seen by all the others. If instance A added an item to self._array, instance B’s self._array would have it as well.

That was not at all what I wanted. So I’ve gone back to overriding init whenever I need to initialize instance variables.


objc.selector and objc.signature

July 12, 2007

I’ve been trying to learn PyObjC by going through Aaron Hillegass’s excellent Cocoa Programming for Mac OS X, doing the exercises in Python instead of Objective C. For the most part the translation between Python and Objective C provided by PyObjC is remarkably seamless, but of course I immediately ran into one of the seams.

I was trying to bind an NSTableView to an array stored in a model object, by way of an NSArrayController. The array was named “employees”, and so I needed to implement collection accessors for inserting and removing items, following the patterns insertObject:in<Key>AtIndex: and removeObjectFrom<Key>AtIndex:, where <Key> is the capitalized name of the collection. So in Objective C these would look like:


- (void) insertObject:(id) obj inEmployeesAtIndex: (unsigned int) index;
- (void) removeObjectFromEmployeesAtIndex: (unsigned int) index;

The translation to Python is simple, just replace colons with underscores and add the self parameter:


def insertObject_inEmployeesAtIndex_(self, obj, index):
def removeObjectFromEmployeesAtIndex_(self, index):

I ran the app, and got an error as soon as I tried to remove an item from my table, causing the removeObjectFromEmployeesAtIndex_ method to be called. It seemed that the index parameter was being set to None (Python’s nil). How was that happening?

The problem was the type of the index parameter — Cocoa was passing an unsigned int, which happened to be zero. But PyObjC defaults to treating all parameters as objects, so that zero was treated as the nil object. PyObjC knows about the methods of lots of Cocoa classes, and can construct special cases so they all work automatically, but it couldn’t know about this call — it couldn’t know that I’d name my array employees, and therefore need those specific collection accessors. I had to find a way to tell PyObjC that my functions take an unsigned int as a parameter.

Unfortunately, the documentation for how to do this is scattered around. Examples are mentioned in passing in the introduction and How to wrap an Objective-C class library documents, and I got the idea that the selector and signature functions in the objc module were involved. So I fired up the Python interpreter and typed print objc.selector.__doc__ and print objc.signature.__doc__. But the most helpful reference was Geoff Wilson’s Showing a NSSavePanel as a sheet, a blog post that described solving a similar problem with an NSSavePanel callback.

The solution, was to follow the definition of insertObject_inEmployeesAtIndex_ with:

insertObject_inEmployeesAtIndex_ = objc.selector(
insertObject_inEmployeesAtIndex_,
signature = 'v@:@I'
)

and to follow the definition of removeObjectFromEmployeesAtIndex_ with:

removeObjectFromEmployeesAtIndex_ = objc.selector(
removeObjectFromEmployeesAtIndex_,
signature = 'v@:I'
)

In Python 2.4 it’s a bit simpler; you can preceed the definition of insertObject_inEmployeesAtIndex_ with:

@objc.signature('v@:I')

and the definition of removeObjectFromEmployeesAtIndex_ with:

@objc.signature('v@:I')

objc.selector or objc.signature tell PyObjC what sort of arguments to expect from Objective C. Figuring out the signature is the one tricky part, but fortunately these functions are simple. Apple documents the supported type encodings. For example, 'v@:I' indicates a function that returns void ('v'), is an object method ('@:'), and takes one unsigned integer argument ('I'). Things get more complicated with pointers, output parameters, etc. I do think it would be helpful if the PyObjC documentation included an extensive collection of example method signatures, along with a more explicit explanation for when objc.selector and objc.signature are required.