[Python-3000] Adaptation [was:Re: Iterators for dict keys, values, and items == annoying :)]

Alex Martelli aleaxit at gmail.com
Sun Apr 2 18:54:30 CEST 2006


On Apr 2, 2006, at 7:11 AM, Paul Moore wrote:
    ...
> On the face of it, there's nothing implicit going on. There has to be
> an explicit adaptation call. However, I agree that systems that make
> extensive use of adaptation can seem to end up in a situation where
> data seems to magically appear without anybody providing it. Black
> magic of that level seems to me to be a clear abuse, though. The

I believe that such "magically appearing" does not depend on  
adaptation, per se, but on the mix of "convenience" approaches to  
adaptation and registration that one chooses to provide alongside it.

With the simplistic scheme I illustrated (protocols denoted by unique  
strings, adaptation occurring only from an object's exact [leafmost]  
type to a specific protocol depending on registration, not even  
support for inheritance of types, much less of protocols, no  
transitivity whatsoever, ...) there is exactly zero risk of anything  
"magically appearing".

Of course, the flip side of that is the inconvenience of having to  
register everything.  I do believe that inconvenience can be reduced  
by supporting mechanisms that Python programmers are already very  
familiar to, such as inheritance of types: nobody finds anything  
strange if, after declaring 'class X(Y):...', they find that  
instances of X "magically acquire" attributes supply by Y -- after  
all, that IS roughly the whole POINT of doing inheritance;-).  I  
believe that, similarly, nothing will be found surprising if, along  
with attributes, X's instances inherit the adaptations enjoyed by Y  
(of course, inherited adaptations can be overridden, just like  
inherited attributes can).

But supplying something like the "duck adaptation" that Brett is so  
keen on might easily destroy this kind of transparency: essentially,  
that would be saying something like """forget the twaddle about  
syntax, semantics and pragmatics: we won't even check half of SYNTAX  
compatibility [[the fact that methods need to be signature- 
compatible]], just the possibly-accidental coincidence of method  
NAMES, and we'll have a hearty laugh at the expense of anybody  
foolish enough to think that "T conforms to protocol P" should mean  
"I have studied the documentation about all the constraints of P, and  
the implementation of T, and I certify that the latter meets the  
former with no need for any wrapping"...quack, quack!""".

After all, we can all plainly see that "class GraphicArtist" supplies  
a method call "draw", and since that's what protocol  
'wild.west.gunslinger' syntactically requires (net of signature- 
compatibility), we can obviously claim that all graphic artists are  
gunslingers, quack quack.

Now if THAT was part of one's adaptation/registration machinery, I  
can well see it as problematic enough for the poor artist who finds  
himself cast in a duel at the OK Corrall.

To a lesser extent, "inheritance of protocols" might sometimes cause  
surprises (though Philip Eby has extensive experience with it and  
claims otherwise, I do not know how much of his complete lack of  
surprises and confusion in the matter might depend on his personal  
skills and not be applicable to many other programmers).  That would  
be a feature like: a protocol P1 might be declared as "inheriting  
from" (or "adaptable to") another protocol P2.  Then, when type T is  
adaptable to P1, and we're looking for an adaptation of T to P2, we'd  
walk a path T -> P1 -> P2 -- possibly with two wrappers in play  
(unless protocol inheritance does not support any true wrapping, just  
identity; in the case the one and only wrapper, is any, is the one  
for the T -> P1 adaptation).  This seems attractive, but at least  
potentially it does mean there might be multiple ways to get from T  
to P2, and it's possible that not all ways would be obvious to an  
observer.  This also applies to inheritance of types, but in that  
case we're all perfectly used to Python solving such "ambiguities" by  
walking T's MRO, so I contend there would arise no surprise nor  
ambiguity; for adaptation of protocol to protocol, we don't have a  
similar intuition based on solid experience to see us through.


>> OTOH, there may be a hidden assumption among the fans of  
>> adaptation that
>> adaptation to a mutable interface should never add state to, nor  
>> copy the
>> state of, an adapted object. Any mutation made via an adaptor  
>> would be
>> reflected as a mutation of the original object. Adaptation to  
>> immutable
>> interfaces would always be fine, naturally. If that's an unwritten  
>> rule of
>> adaptation, then:
>>    1. It addresses the main evil of implicit type conversion  
>> (hidden state)
>>    2. It needs to become a *written* rule, so that anyone writing  
>> a stateful
>> adapter can be duly admonished by their peers
>
> I don't know if that's an "unwritten rule" as such - but I can barely
> imagine what you're describing as unacceptable (adaptation to a
> mutable interface which adds or copies state). It just seems like a
> stupid thing to do (or at least, not at all what adaptation is about).
> But maybe that's what you mean by a "hidden assumption".

Uh?  Consider iteration -- that's a prime example of an adaptation  
which adds "hidden state", and I don't see it as particularly  
problematic to frame as an adaptation.

When I adapt an instance L of list to 'org.python.stdlib.iterator', I  
add one bit of state -- the "current index".  I mutate that state  
each and every time I call .next() on the resulting object, and that  
state is not at all reflected on the original list, which in fact is  
totally unaware of whether there are iterators outstanding on it, and  
if so, how many, and in which states.

Why is this seen as a problem?

> Regardless, I'd have no problem with a style guide, or good practice
> document, stating that this is what adaptation is about, and stateful
> adapters are bad practice. (That's just my opinion - better check this
> with people who make heavy use of adaptation). But to me it feels like
> labouring the obvious - along the lines of explicitly prohibiting
> metaclass or decorator abuse.

Do we have a "good practice document" about what you should or  
shouldn't do with metaclasses, or decorators, or, for that matter,  
with inheritance, operator overloading, and other powerful constructs  
and tools that have been with Python a long, long time?

>> The other thing is that it makes more sense to me for there to be a
>> per-protocol type->adapter registry, rather than a global registry  
>> with tuples
>> of source type/target protocol pairs.
>
> What difference would that make in practice?

Not all that much: I see this as a mere implementation detail.  You  
can implement a mapping from (A,B) to C in two ways (and, no doubt,  
many others yet):

-- the most direct one: a dict with (A,B) as the key, C as the value
-- an indirect one: a dict with A as the key, whose value is a dict  
with B as the key and C as the value

If all you do is lookups from (A,B) to get C, the former is simpler;  
the latter may be faster when you need to look up all B's  
corresponding to an A, since it makes that operation O(1) rather than  
O(N).  So, if you add to the adapt and register primitives other  
primitives for "give me all protocols to which T can be adapted", or  
"give me all types which can adapted to P", choosing the "right"  
nested-dict implementation might make one of these lookups (not both)  
faster.  Probably worth doing only if such lookup are indeed  
frequent; since I believe their frequency of use will be tiny (as  
will the number of calls to registration) compared with the number of  
calls to adapt, I'd go all out to optimize the latter, first and  
foremost -- after which, one can pick tradeoffs between cost of  
registration and costs of other kinds of lookups (after all, one  
might also want to see "all (A,B)s for a given C", no?-).  For  
example, keep the "most direct one" dict, and add auxiliary ones to  
support other lookups -- this makes registration slower (but it's a  
very rare operation anyway) and takes up a bit more memory (but, I  
believe we're talking pennies), but can speed up all kinds of lookups.

Exactly why we're so prematurely discussing fine-tuning-level  
optimization concerns, at this stage, escapes me a bit, though.

>> Secondly, given that each framework is likely to be defining the  
>> protocols
>> that it consumes, I don't see the problem with each one defining  
>> its *own*
>> adaptation registry, rather than having one mega-registry that adapts
>> everything to everything.
> [...]
>> Then the role of an adaptation module in the standard library  
>> would be to
>> provide a standard API for per-framework registries, without also  
>> providing a
>> mega-registry for adapting everything to everything.
>
> Not an unreasonable idea, but how valuable would it be in practice?
> Alex's proposal allowed for explicitly specifying a registry, while
> still having a default "central" registry. For 99% of use, I'd suspect
> that people would not bother with a special registry. And if protocols

I don't think this other tweak would be a _big_ "bother", but neither  
would it be at all useful, just a medium-level useless bother.

Say, for example, that protocols are identified (as in my strawman  
proposal) by unique strings anyway. E.g., if I were to invent a  
protocol, I could name it 'it.aleax.myprot' -- since I own the  
aleax.it domain, nobody else could create a name conflict.  Saying  
that each framework has a separate registry is just the same as  
saying that each protocol "lives" in one specific registry, so that  
any registration or lookup regarding protocol P *must* also specify  
registryof(P).  Hopefully, rather than having to keep this  
correspondence in our human memory, we're allowed to have a registry  
of registries which remembers this correspondence for you: we can  
register_protocol(P, registry) and we can lookup the registry for a  
given protocol with function registryof.  E.g.:

_reg_of_regs = {}
def register_protocol(P, registry): _reg_of_regs[P] = registry
def registryof(P): return _reg_of_regs[P]

So now, all calls which, in my proposal, would be (e.g.) adapt(x, P),  
must instead become adapt(x, P, registryof(P)).

Not a big bother, just an amount of totally useless boilerplate  
that's just sufficient to be annoying, it seems to me.  Of course, if  
the registry of registries was somehow forbidden, then the bother  
WOULD become bigger, since in that case the poor programmer would  
have to mentally memorize, or continually look up (with grep, Google  
search, or similar means) the total equivalent of the registryof(P)  
function result.

I may be missing something here, I guess, because I just don't see  
the point.

> were defined via some "interface" approach (like zope.interfaces and
> PyProtocols do) then encapsulation is taken care of by uniqueness of
> types/interfaces. I know interfaces are outside the scope of what's
> being proposed right now, but one of their benefits is that they *do*
> solve this problem. Structured strings naming protocols
> ("org.python.std.index" or whatever) do this as well, but without
> language support.

I did mention that one issue with my "strawman proposal" was exactly  
that it performs no error checking: it entirely relies on programers  
respecting some simple and reasonable conventions, rather than piling  
up machinery to provide enforcement. Much like, oh, say, Python.

Isn't it just wonderful, how the foes of adaptation switch horses on  
you?  First they request a simple-as-dirt, bare-bones "example  
system" -- then as soon as you provide one they come back at you with  
all sort of "cruft" to be piled on top.  Ah well, having tried to  
evangelize for adaptation for years, I've grown wearily accustomed to  
this kind of response; it sometimes looks more like such foes are  
feeling defensive, and ready to pull any trick to stop adaptation  
from getting in the language, rather than interested in the various  
technica aspect of the issue.  To be fair, this isn't all that  
different from the average reaction one always gets from python-dev  
as a whole to any proposal whatsoever.  Anyway, I hope it's clearer  
now why, each and every time, I end up giving up, and deciding that  
beating my head against a wall is more productive and fun;-).

I guess I'll just adopt a signature of "Praeterea censeo adaptatio  
esse adoptanda!", for all the good that all the detailed discussions  
appear to have been doing;-).


Alex



More information about the Python-3000 mailing list