[Python-Dev] PEP 246: lossless and stateless
Phillip J. Eby
pje at telecommunity.com
Fri Jan 14 08:38:05 CET 2005
At 10:20 PM 1/13/05 -0800, Guido van Rossum wrote:
>[Guido]
> > >This may solve the curernt raging argument, but IMO it would make the
> > >optional signature declaration less useful, because there's no way to
> > >accept other kind of adapters. I'd be happier if def f(X: Y) implied X
> > >= adapt(X, Y).
>
>[Phillip]
> > The problem is that type declarations really want more guarantees about
> > object identity and state than an unrestricted adapt() can provide,
>
>I'm not so sure. When I hear "guarantee" I think of compile-time
>checking, and I though that was a no-no.
No, it's not compile-time based, it's totally at runtime. I mean that if
the implementation of 'adapt()' *generates* the adapter (cached of course
for source/target type pairs), it can trivially guarantee that adapter's
stateless. Quick demo (strawman syntax) of declaring adapters...
First, a type declaring that its 'read' method has the semantics of
'file.read':
class SomeKindOfStream:
def read(self, byteCount) like file.read:
...
Second, third-party code adapting a string iterator to a readable file:
def read(self, byteCount) like file.read for type(iter("")):
# self is a string iterator here, implement read()
# in terms of its .next()
And third, some standalone code implementing an "abstract" dict.update
method for any source object that supports a method that's "like"
dict.__setitem__:
def update_anything(self:dict, other:dict) like dict.update for object:
for k,v in other.items(): self[k] = v
Each of these examples registers the function as an implementation of the
"file.read" operation for the appropriate type. When you want to build an
adapter from SomeKindOfStream or from a string iterator to the "file" type,
you just access the 'file' type's descriptors, and look up the
implementation registered for that descriptor for the source type
(SomeKindOfStream or string-iter). If there is no implementation
registered for a particular descriptor of 'file', you leave the
corresponding attribute off of the adapter class, resulting in a class
representing the subset of 'file' that can be obtained for the source class.
The result is that you generate a simple adapter class whose only state is
a read-only slot pointing to the adapted object, and descriptors that bind
the registered implementations to that object. That is, the descriptor
returns a bound instancemethod with an im_self of the original object, not
the adapter. (Thus the implementation never even gets a reference to the
adapter, unless 'self' in the method is declared of the same type as the
adapter, which would be the case for an abstract method like 'readline()'
being implemented in terms of 'read'.)
Anyway, it's therefore trivially "guaranteed" to be stateless (in the same
way that an 'int' is "guaranteed" to be immutable), and the implementation
is also "guaranteed" to be able to always get back the "original" object.
Defining adaptation in terms of adapting operations also solves another
common problem with interface mechanisms for Python: the dreaded "mapping
interface" and "file-like object" problem. Really, being able to
*incompletely* implement an interface is often quite useful in practice, so
this "monkey see, monkey do" typing ditches the whole concept of a complete
interface in favor of "explicit duck typing". You're just declaring "how
can X act 'like' a duck" -- emulating behaviors of another type rather than
converting structure.
>Are there real-life uses of stateful adapters that would be thrown out
>by this requirement?
Think about this: if an adapter has independent state, that means it has a
particular scope of applicability. You're going to keep the adapter and
then throw it away at some point, like you do with an iterator. If it has
no state, or only state that lives in the original object (by tacking
annotations onto it), then it has a common lifetime with the original object.
If it has state, then, you have to explicitly manage that state; you can't
do that if the only way to create an adapter is to pass it into some other
function that does the adapting, unless all it's going to do is return the
adapter back to you!
Thus, stateful adapters *must* be explicitly adapted by the code that needs
to manage the state.
This is why I say that PEP 246 is fine, but type declarations need a more
restrictive version. PEP 246 provides a nice way to *find* stateful
adapters, it just shouldn't do it for function arguments.
> > Even if you're *very* careful, your seemingly safe setup can be blown just
> > by one routine passing its argument to another routine, possibly causing an
> > adapter to be adapted. This is a serious pitfall because today when you
> > 'adapt' you can also access the "original" object -- you have to first
> > *have* it, in order to *adapt* it.
>
>How often is this used, though? I can imagine all sorts of problems if
>you mix access to the original object and to the adapter.
Right - and early adopters of PEP 246 are warned about this, either from
the PEP or PyProtocols docs. The PyProtocols docs early on have dire
warnings about not forwarding adapted objects to other functions unless you
already know the other method needs only the interface you adapted to
already. However, with type declarations, you may never receive the
original object.
> > But type declarations using adapt()
> > prevents you from ever *seeing* the original object within a function. So,
> > it's *really* unsafe in a way that explicitly calling 'adapt()' is
> > not. You might be passing an adapter to another function, and then that
> > function's signature might adapt it again, or perhaps just fail because you
> > have to adapt from the original object.
>
>Real-life example, please?
If you mean, an example of code that's currently using adapt() that I'd
have changed to use type declaration instead and then broken something,
I'll have to look for one and get back to you. I have a gut feel/vague
recollection that there are some, but I don't know how many.
The problem is that the effect is inherently non-local; you can't look at a
piece of code using type declarations and have a clue as to whether there's
even *potentially* a problem there.
>I can see plenty of cases where this could happen with explicit
>adaptation too, for example f1 takes an argument and adapts it, then
>calls f2 with the adapted value, which calls f3, which adapts it to
>something else. Where is f3 going to get the original object?
PyProtocols warns people not to do this in the docs, but it can't do
anything about enforcing it.
>But the solution IMO is not to weigh down adapt(), but to agree, as a
>user community, not to create such "bad" adapters, period.
Maybe. The thing that inspired me to come up with a new approach is that
"bad" adapters are just *sooo* tempting; many of the adapters that we're
just beginning to realize are "bad", were ones that Alex and I both
initially thought were okay. Making the system such that you get "safe"
adapters by default removes the temptation, and provides a learning
opportunity to explain why the caller needs to manage the state when
creating a stateful adapter. PEP 246 still allows you to leave it implicit
how you get the adapter, but it still should be created explicitly by the
code that needs to manage its lifetime.
> OTOH there
>may be specific cases where the conventions of a particular
>application or domain make stateful or otherwise naughty adapters
>useful, and everybody understands the consequences and limitations.
Right; and I think that in those cases, it's the *caller* that needs to
(explicitly) adapt, not the callee, because it's the caller that knows the
lifetime for which the adapter needs to exist.
> > Clark's proposal isn't going to solve this issue for PEP 246, alas. In
> > order to guarantee safety of adaptive type declarations, the implementation
> > strategy *must* be able to guarantee that 1) adapters do not have state of
> > their own, and 2) adapting an already-adapted object re-adapts the original
> > rather than creating a new adapter. This is what the monkey-typing PEP and
> > prototype implementation are intended to address.
>
>Guarantees again. I think it's hard to provide these, and it feels
>unpythonic.
Well, right now Python provides lots of guarantees, like that numbers are
immutable. It would be no big deal to guarantee immutable adapters, if
Python supplies the adapter type for you.
>(2) feels weird too -- almost as if it were to require
>that float(int(3.14)) should return 3.14. That ain't gonna happen.
No, but 'int_wrapper(3.14).original_object' is trivial.
The point is that adaptation should just always return a wrapper of a type
that's immutable and has a pointer to the original object.
If you prefer, call these characteristics "implementation requirements"
rather than guarantees. :)
>Or maybe we shouldn't try to guarantee so much and instead define
>simple, "Pythonic" semantics and live with the warts, just as we do
>with mutable defaults and a whole slew of other cases where Python
>makes a choice rooted in what is easy to explain and implement (for
>example allowing non-Liskovian subclasses). Adherence to a particular
>theory about programming is not very Pythonic; doing something that
>superficially resembles what other languages are doing but actually
>uses a much more dynamic mechanism is (for example storing instance
>variables in a dict, or defining assignment as name binding rather
>than value copying).
Obviously the word "guarantee" hit a hot button; please don't let it
obscure the actual merit of the approach, which does not involve any sort
of compile-time checking. Heck, it doesn't even have interfaces!
More information about the Python-Dev
mailing list