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

Walter Dörwald walter at livinglogic.de
Mon Apr 3 11:21:26 CEST 2006


Alex Martelli wrote:

> On Apr 2, 2006, at 4:39 PM, Walter Dörwald wrote:
>    ...
>> Why not make the registry identical to the protocol? The protocol is 
>> just a convention anyway:
> 
> Yes, a 1<->1 relationship between registries and protocols makes the 
> 'registryof' function I was talking about simpler, at least when it's 
> implemented as identity.  What's the advantage of this approach, 
> compared to allowing protocols to be arbitrary hashable attributes?  The 
> disadvantage, essentially, is that one gives up the degrees of freedom 
> implied by protocols being "just a convention": a protocol must now 
> (say) be a callable object exposing a register method (with given 
> signature and semantics).

Yes, but otherwise the adapter has to be, so this doesn't buy us much.

> I guess adopting that kind of constraint is OK, as long as the 
> constraint doesn't in any way interfere with whatever else Guido wants 
> to do with protocols and thus give him a reason to reject adaptation. I 
> guess I can see some potential for minor optimization: a protocol that's 
> written with some existing types already in mind might use a subclass of 
> your 'Adaptor', such as:
> 
> class FastTracker(Adaptor):
>     def __init__(self, types):
>         self.fastrack = set(types)
>         Adaptor.__init__(self)
>     def _anyof(self, types):
>          for t in types:
>              if t in self.fastrack: return True
>          return False
>     def register(self, adaptor, types):
>         if self._anyof(types):
>             # need better diagnostics here, of course
>             raise RegistrationError, "Cannot override identity-adaptation"
>         return Adaptor.register(self, adaptor, types)
>     def __call__(self, obj, *a, **k):
>         if self._anyof(type(obj).__mro__):
>             if a or k:
>                 raise ...some kind of diagnostics about a/k not allowed 
> here...
>             return obj
>         return Adaptor.__call__(self, obj, *a, **k)
> 
> I'm not sure the advantage is big enough to warrant all the machinery, 
> but I can't assert it isn't, either.

OK, this is the version where you know that some types implement the 
protocol itself, but you can't register those types via an attribute on 
the types.

>>> 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.
>>
>> I think you might be misinterpreting reactions. If the initial 
>> reaction was "I don't understand it. Nobody needs this." (at least 
>> that was my reaction), you're "strawman proposal" has put us past 
>> this. (At least you got two "I finally got it, this seems useful" from 
>> me and Brett.)
> 
> You're quite likely right, and I apologize for the misinterpretation; I 
> guess I may have misread Brett's desire to hammer adaptation's very 
> reason for being down to miniscule smithereens by having the existence 
> of some set of methods "imply" identity-adaptation, as a subconscious 
> desire to defend against the whole idea of adaptation in the guise of 
> sort-of-accepting it, for example.

I guess it's more an attempt to compare adaption to something we already 
know. I fact getattr() is the stupid version of adapation. There's only 
identity adaption and the protocol is a method or an attribute! ;)

>> So now lets answer the questions: How do we implement adaption of 
>> subtypes? What is a protocol? How can we make registration as painless 
>> as possible?  etc.
> 
> Looks like the "loop on __mro__" idea is sort of standard for the first 
> of these questions.

Yes, probably with a hand crafted version of a mro for classic classes.

The big advantage of using the mro is that performance is independent 
from the number of registered adapter functions.

> As for "what is a protocol", I'd rather minimize 
> the machinery that goes with it, but I guess that's some kind of 
> holdover from the "strawman proposal"; if we're making protocols into 
> classes anyway, I'd like to provide them with a check_compliance method, 
> taking an object and an integer that represents the amount of effort to 
> be spent in the checking -- 0 meaning 'just check existence of methods', 
> 1 meaning 'check methods' signature compatibility too', 2 meaning 'check 
> some semantics at a level suitable for fast unit-tests', and so on up.

I don't see how that buys us much (And it looks like you're trying to 
solve the halting problem ;)

class Foo:
    __implements__ = [Bar]

    def bar(self):
       if Bar.check_compliance(self, 2):
          # don't comply
       else:
          # comply


> The author of a protocol doesn't have to do all that much (levels 0 and 
> 1 can be provided by helper functions/methods relying on the inspect 
> module), but he or she CAN make the protocol into "an executable 
> specification" at whatever level he or she desires.

I'm really not sure how this would look in practice.

> Other error checking 
> (e.g., against registration of multiple adapters for one protocol/type 
> pair) as well as some modest optimization (mostly of adaptation, the 
> frequent case) may also be warranted.
> 
> I'm not sure what's painful at all about registration -- at worst it's 
> one call per type/protocol pair,

Exactly, especially for the case where we already have this adaption 
machinery today (i.e. copy_reg).

> some refactoring such as yours making 
> it even slighter by allowing the registration of several pairs in one 
> call.  Do you mean that people are so keen on declarative styles that 
> they'd crave, e.g., some special machinery such as
> 
> class Duck(object):
>     __implements__ = walk, talk, quack
>     ...
> 
> (with the metaclass doing the registration calls, with identity 
> adaptation, under the cover for each protocol in __implements__), 
> prizing it highly over explicit:

There's no need for registration calls in this case. The 
protocol/adapter can do it:

class Adapter(object):
    # __init__() and register() as before.

    def __call__(self, obj, *args, **kwargs):
       objtype = type(obj)
       if hasattr(objtype, "__implements__") and self in 
objtype.__implements__:
          return obj
       # rest of the code as before

> class Duck(object):
>    ...
> adapt.register(Duck, (walk, talk, quack), identity)
> 
> or the loop or classdecorator version of the latter?  Ah well, maybe, 
> but most of the discussion sort of smacks to me of AGNI and/or premature 
> optimization.
> 
> Could we agree that simplicity and even minimalism (with a side order of 
> future extensibility if/when warranted) are high on the list of desired 
> properties for a protocol adaptation mechanism?

Absolutely!

> After all, the more 
> machinery, cruft, and black magic we pile on top of adaptation's simple 
> core ideas, the more likely the whole caboodle is to provoke allergic 
> reactions in anybody who's not being a part of this discussion...

True. Make it simple enough to cover 80% of the cases and be 
understandable, but extensible enough, so we can add anything that might 
come up in the future without having to contort the adaption protocol so 
much that it's no longer recognizable.

 From a higher point of view, this is one of the few occasions where a 
Design Pattern makes sense in Python. Usually this means that the 
language isn't high level enough (nobody implements the "Factory 
Function Pattern" in Python because classes are callable). If Python had 
multi methods we wouldn't be discussing adaption, because basically what 
we're discussing here is how to implement double dispatch.

Bye,
    Walter Dörwald



More information about the Python-3000 mailing list