[Python-Dev] Concrete proposals for PEP 246
Phillip J. Eby
pje at telecommunity.com
Tue Jan 11 21:53:08 CET 2005
To save everyone from having to wade through the lengthy discussions
between Alex and I, and to avoid putting all the summarization burden on
Alex, I thought I would start a new thread listing my concrete proposals
for PEP 246 changes, and summarizing my understanding of the current
agreements and disagreements we have. (Alex, please correct me if I have
misrepresented your position in any respects.)
First, I propose to allow explicitly-declared Liskov violations, but using
a different mechanism than the one Alex has proposed. Specifically, I wish
to remove all type or isinstance checking from the 'adapt()'
function. But, the 'object' type should have a default __conform__ that is
equivalent to:
class object:
def __conform__(self,protocol):
if isinstance(protocol,ClassTypes) and isinstance(self,protocol):
return self
return None
and is inherited by all object types, including types defined by extension
modules, unless overridden.
This approach provides solid backward compatibility with the previous
version of PEP 246, as long as any user-implemented __conform__ methods
call their superclass __conform__. But, it also allows Liskov-violating
classes to simply return 'None' to indicate their refusal to conform to a
base class interface, instead of having to raise a special exception. I
think that, all else being equal, this is a simpler implementation approach
to providing the feature, which Alex and others have convinced me is a
valid (if personally somewhat distasteful) use case.
Second: Alex has agreed to drop the "cast" terminology, since its meanings
in various languages are too diverse to be illuminating. We have instead
begun creeping towards agreement on some concepts such as "lossy" or
"noisy" conversions. I think we can also drop my original "lossy" term,
because "noisy" can also imply information loss and better explains the
real issue anyway.
A "noisy" conversion, then, is one where the conversion "makes up"
information that was not implicitly present in the original, or drops
information that *alters the semantics of the information that is
retained*. (This phrasing gets around Alex's LotsOfInfo example/objection,
while still covering loss of numeric precision; mere narrowing and renaming
of attributes/methods does not alter the semantics of the retained
information.)
Adaptation is not recommended as a mechanism for noisy conversions, because
implicit changes to semantics are a bad idea. Note that this is actually
independent of any transitivity issues -- implicit noisy conversion is just
a bad idea to start with, which is why 'someList[1.2]' raises a TypeError
rather than implicitly converting 1.2 to an integer! If you *mean* to drop
the .2, you should say so, by explicitly converting to an integer.
(However, it *might* be acceptable to implicitly convert 1.0 to an integer;
I don't currently have a strong opinion either way on that issue, other
than to note that the conversion is not "noisy" in that case.)
Anyway, I think that the current level of consensus between Alex and myself
on the above is now such that his comparison to casting could now be
replaced by some discussion of noisy vs. non-noisy (faithful?
high-fidelity?) conversion, and the fact that adaptation is suitable only
for the latter, supplemented by some examples of noisy conversion use cases
and how to transform them into non-noisy constructs. The string->file vs.
string->file_factory example is a particularly good one, I think, because
it shows how to address a common, practical issue.
Third: (Proposed) The PEP should explicitly support classic classes, or
else there is no way to adapt exception instances. (Yes, I have actually
done this; peak.web adapts exception instances to obtain appropriate
handlers, for example.)
Fourth: The principal issue from the original discussion that remains open
at this time is determining specific policies or recommendations for
addressing various forms of transitivity, which we have delved into a
little bit. (By "open" I jut mean that there is no proposal for this
issue currently on the table, not to imply that my proposals are not also
"open" in the sense of awaiting consensus.)
Anyway, the kinds of transitivity we're discussing are:
1. interface inheritance transitivity (i.e. adapt(x,IBase) if
adapt(x,IDerived) and IDerived inherits from IBase)
2. adapter composition transitivity (i.e. adapt(x,ISome) if
adapt(x,IOther) and there is a general-purpose ISome->IOther adapter available.
These are mostly issues for the design of an interface system (implementing
__adapt__) and for the design of a global adapter registry. I don't think
it's practical to implement either kind of transitivity on the __conform__
side, at least not for hand-written __conform__ methods.
To summarize current PEP 246 implementations' choices on this issue, Zope
implements type 1 transitivity, but not type 2; PyProtocols implements
both. Both Zope and PyProtocols allow for individual objects to assert
compliance with an interface that their class does not claim compliance
with, and to use this assertion as a basis for adaptation.
In the case of PyProtocols, this is handled by adding a per-instance
__conform__, but Zope has a separate concept of declaring what interfaces
an instance "provides", distinct from what it is "adaptable
to". PyProtocols in contrast considers "provides" to be the same as
"adaptable to with no adapter", i.e. a trivial special case of adaptation
rather than a distinct concept.
I have also asserted that in practice I have encountered more problems with
type 1 transitivity than with type 2, because of the strong temptation to
derive an interface to avoid duplicating methods. In other words,
inappropriate use of interface inheritance produces roughly the same effect
as introducing a noisy adapter into a type 2 adapter mesh, but IMO it's
much easier to do innocently and accidentally, as it doesn't even require
that you write an adapter!
More information about the Python-Dev
mailing list