What is adaptation?
This document was originally posted as a reply to a c.l.py post asking for more information on adaptation vs type checking for Python. It was well received, and I thought it deserved a spot here. This is a revised edition, with more information and some clarifications.
Adaptation is the act of taking one object and making it conform to a given protocol (or interface). Adaptation is the key to make dynamic code that takes parameters from arbitrary types work in a safe, well behaved way.
The basic concept underlying adaptation is the protocol, also called interface in some implementations. For all purposes of this discussion, and for simplicity reasons, we can safely assume that protocols and interfaces are equivalent (more on this later).
A protocol defines how an object should behave in a given situation. It defines both a set of primitives that must be supported by the object, and its expected behavior -- how is it supposed to work, and how it should be used in a real case scenario. For example: the iterator protocol defines the following primitives: __iter__ and next() (see the typeiter.html documentation). The documentation also tells that what the primitives do; for example, next() returns the next element of the iterator, and raises an StopIterator exception when it finishes. Any object from any class that implement these methods with the expected behavior, regardless of anything else (other methods it supports, or its ancestors), is said to support the iterator protocol.
Any object that supports the iterator protocol can be used whenever an iterable is acceptable. This includes for loops and list comprehensions. The biggest advantage of adaptation comes when one realize how flexible this design is, specially when compared with old-style type checking. In a old-style strict type checking environment (such as C++), parameters to a given routine must conform to the declared type of the arguments. For iterators, it would mean that only objects descending from a standard base class (let's say, "Iterable") would be accepted. Complex objects have to support multiple protocols, though. Multiple inheritance can be used to the rescue, but the final design becomes complex and inflexible.
Now, back to Python world. To support a protocol, all you need to do is to implement it. Although one can still use multiple inheritance to declare new classes with multiple protocols, this is not needed. In most cases, the resulting object can be used directly whenever the support for the protocol is required, with no need for adaptation, and without concern about strict type checking.
But there are situations when the object itself can't be immediately used; it has to be adapted, or prepared, to support the protocol. The adapt() call implements all the necessary magic to check whether the object supports a protocol, and to make the necessary adaptations (if any), returning a conformant object. The adaptation will fail if the object does not support the protocol; this is an error, that can be catched by adapt() in a superficially similar but fundamentally different approach from type checking.
The adapt protocol (as presented on PEP246) defines a very flexible framework to adapt one object to a protocol. It tries a number of alternatives for adaptation; for example, the object may adapt itself to the protocol, or a registered adapter function may be used. The result of the adaptation (if possible at all) is an object that is guaranteed to support the protocol. So, using adapt(), we can write code like this:
for item in adapt(obj, Iterable):
Of course, this is a simple example, but it is useful to understand the basic mechanism. After PEP246 was published, other alternative implementations were published. The PyProtocols package somewhat extends the concept.
Finally, one may be wondering, is there any situation when an object needs to be adapted? Why don't just check for the availability of the interface? There are many reasons to use the adapt framework. The protocol checking is just one of the reasons -- it allows errors to be
catched much earlier, and at a better location. Another possible reason is that complex objects may support several protocols, and there may be name clashes between some of the methods. One such
situation is when an object support different *versions* of the same protocol. All versions have the same method names, but semantics may differ slightly. The adapt() call can build a new object with the correct method names and signatures, for each protocol or version supported by the object. Finally, the adaptation method can optionally build an opaque "proxy" object, that hides details of the original methods signature, and it's thus safer to pass around.
Adaptation shines when used with complex frameworks. Each framework define lots of protocols, and there are often discrepancies (or mismatches), and an adapter in-between is required. The adaptation system (as implemented by PyProtocols) supports a global register of adapter functions. Using adapt() at convenient locations, it's posible to mix and match objects provided by different frameworks, with no need to worry about compatibility issues. The work may be done just once, on the adapter; once registered, the adapt() calls will take care of all necessary work.
Using adaptation effectively requires discipline. It's too easy to get lazy and forget to include adapt() calls at the required locations. But the advantages are immense, for the adaptation system preserves Python dynamic aspects while adding still more flexibility to the package. It's a great addition to an already great language.
Protocols and interfaces are similar concepts, but not equivalent. Interfaces are just the set of methods and their individual semantics; it does not define the "how to use" part as a protocol does. However, for all practical purposes, the concepts converge, because it does not make much sense to keep with the strict static protocol definition for long.
This document was started as my attempt to contribute back to the Python community something which I have learned while reading c.l.py and working with Python. I also have to thank Alex Martelli for his comments and clarifications on my original post, as all the others who have helped me (knowingly or not) over the past few months.