Ian Bicking: the old part of his blog

Adaptation vs. Magic Methods

There are lots of magic methods in Python. Some of them are especially magic, hooking into the underlying object model -- methods like __getattr__, or its newer cousins like __get__ (used in descriptors), or __getattribute__.

I'll refer to a different set of magic attributes, things like __len__, __nonzero__, __gt__, etc. These are generally ways for functions to allow objects to provide overrides -- len(x) calls x.__len__()__, a > b calls a.__gt__(b), etc. Lots of these functions also perform some other fallbacks -- if __gt__ is not found, then __cmp__ is tried, and finally the comparison is done based on some obscure comparison of class names, or IDs or something. When testing a value for truth, __nonzero__ is called, then if that's not found __len__ (to check if it is zero length) then if that's not found the object is considered "true".

A lot of these methods exist, both inside base Python, the standard library, and extension modules (like ZODB). One could argue that they are a sign of a hackish object model in Python -- Ruby is certainly an instance of a language that does not do this sort of thing. But by using functions (or operators) instead of directly using methods, they do allow naive objects to be handled easily.

It occurred to me -- specifically when thinking about the __iter__ special method -- that many of these are examples of adaptation (I've been using PyProtocols, which is well documented and concisely distributed). In the case of __iter__ we are adapting an object to the iterator interface. __nonzero__ (and friends) adapts to the boolean interface, and so on. v.__class__ could become adapt(v, IType) (since classes are of type type).

In a strange sort of way, this could make things more context-sensitive, like Perl. Unlike Perl the context is explicit (using explicit interfaces, and adapting the object explicitly with adapt()), but like Perl an object could be used and reused in different contexts to provide different kinds of information. Adaptation to an iterator is easy enough to consider -- how would adapation work for __len__? Would we create a ILength interface, to distinguish the unique semantics (but not type!) of length? (Do we need to start annotating our numbers with units to handle this?)

Following this further (too far?) does adaptation start to replace traversal? Instead of out.write('text') do we say adapt(out, IWriter)('text')? We quickly find ourselves using a wildly different object model, and a different notion of object-orientation.

Created 12 Oct '03
Modified 14 Dec '04

Comments:

Hmmm... so *you're* the reason Guido's uncomfortable with how far people might take adaptation. ;)

The v.__class__ example is... I hate to say wrong, but actually it is. If you have an object that's a type already, you'd get back the same object. So, adapting an object to its type doesn't make sense.

In general, I don't think you can make a slots -> interface mapping of the sort you've laid out. I'd be more likely to say that certain slots or combinations could be described by interfaces, and that Python uses the slot presence rather than an adaptation mechanism to find them.

It's true that slots like __int__, __str__, and __nonzero__ are for type conversion, and therefore could be treated in terms of adaptation, though.

And the traversal stuff is also too far out there... *why* would you use that style, when it's so much harder to read?

Ah well, people are going to experiment with this sort of thing in their "wild youth", but hopefully they'll settle down into the "adaptation as type-casting" model where it best fits. :)
# Phillip J. Eby

Oh come on, PJE, let's have some fun and shake up the establishment. :-)

When you're working with some arbitrary object, you don't want to know anything about its implementation. Using methods instead of attributes isolates knowledge of data structures, but you still have to know what interfaces the object supports. Adaptation lets you abstract away even the knowledge of an object's interfaces.

The important consideration is how often you work with arbitrary objects, as opposed to objects under your direct control. In Zope, it turns out that you end up working with arbitrary objects very frequently. In Zope 3, most code is going to be completely agnostic of the actual interfaces objects support.

Just as in days past we programmers made the transition from working directly with data structures to calling methods, maybe in the future, we'll transition to using adapters instead of calling methods. And it could be that programmers will flock to the first language that makes adaptation easier than any other language.

Here is a syntax I've been toying with:

foo.IWrite::write(bar)

This is equivalent to "adapt(foo, IWrite).write(bar)", but hopefully it more clearly expresses that you can think of "write" as a method of nearly any object.

The double colon is nearly borrowed from C++ and XPath, but a lot of Python programmers don't know that stuff. Some other possibilities:

foo.[IWrite]write(bar)

foo.write@IWrite(bar)

(foo as IWrite).write(bar)

Ok, after writing them out, I think the last suggestion is by far the most readable. It requires parentheses, but that could actually be a benefit, since it's the only suggestion that lets you store the adapted object in a variable:

baz = foo as IWrite
baz.write(bar)

The "as" keyword could be implemented through an __adapt__ magic name.

I should write about this on comp.lang.python and see how well it floats. :-)
# Shane Hathaway

I like "as", reads just right. foo.IWrite::write(v) seems all wrong -- foo::IWrite.write(v) seems much better. But, wait, that matches my dislike of how you wanted to use : in path expressions too -- I guess I think of :: as a harder separator than you.

But it doesn't really matter, "as" is perfect. Of course, introducing syntax for a feature that doesn't have a canonical or standard implementation might be premature ;)
# Ian Bicking