Comparison with False - something I don't understand
Carl Banks
pavlovevidence at gmail.com
Mon Dec 6 17:42:15 EST 2010
On Dec 6, 12:58 pm, m... at distorted.org.uk (Mark Wooding) wrote:
> Paul Rubin <no.em... at nospam.invalid> writes:
> > You know, I've heard the story from language designers several times
> > over, that they tried putting resumable exceptions into their languages
> > and it turned out to be a big mess, so they went to termination
> > exceptions that fixed the issue.
>
> That seems very surprising to me.
>
> > Are there any languages out there with resumable exceptions?
>
> Common Lisp and Smalltalk spring to mind. It's fairly straightforward
> to write one in Scheme. (Actually, implementing the Common Lisp one in
> terms of fluids, closures and blocks isn't especially difficult.)
>
> > Escaping to a debugger doesn't really count as that.
>
> Indeed not.
>
> > I guess one way to do it would be call a coroutine to handle the
> > exception, and either continue or unwind after the continue returns,
> > but doing it in a single-threaded system just seems full of hazards.
>
> It seems pretty straightforward to me. Handlers are simply closures;
> the registered handlers are part of the prevailing dynamic context.
> When an exception occurs, you invoke the handlers, most-recently
> registered first. A handler that returns normally can be thought of as
> `declining' to handle the exception; a handler that explicitly transfers
> control elsewhere can be thought of as having handled it.
>
> To make this work, all you need is:
>
> * a fluid list (i.e., one which is part of the dynamic context) of
> handlers, which you can build in pure Python if you try hard enough
> (see below);
>
> * closures to represent handlers, which Python has already, and;
>
> * a nonlocal transfer mechanism, and a mechanism like try ... finally
> to allow functions to clean up if they're unwound.
>
> We can actually come up with a nonlocal transfer if we try, by abusing
> exceptions.
>
> [The code in this article is lightly tested, but probably contains
> stupid bugs. Be careful.]
>
> class block (object):
> """
> Context manager for escapable blocks.
>
> Write
>
> with block() as escape:
> ...
>
> Invoking the `escape' function causes the context body to exit
> immediately. Invoking the `escape' function outside of the
> block's dynamic context raises a ValueError.
> """
> def __init__(me):
> me._tag = None
> def _escape(me, value = None):
> if me._tag is None:
> raise ValueError, 'defunct block'
> me.result = value
> raise me._tag
> def __enter__(me, value = None):
> if me._tag:
> raise ValueError, 'block already active'
> me._tag = type('block tag', (BaseException,), {})
> me.result = value
> return me._escape
> def __exit__(me, ty, val, tb):
> tag, me._tag = me._tag, None
> return ty is tag
>
> This is somewhat brittle, since some intervening context might capture
> the custom exception we're using, but I don't think we can do
> significantly better.
>
> Implementing fluids badly is easy. Effectively what we'd do to bind a
> fluid dynamically is
>
> try:
> old, fluid = fluid, new
> ...
> finally:
> fluid = old
>
> but this is visible in other threads. The following will do the job in
> a multithreaded environment.
>
> import threading as T
> import weakref as W
>
> class FluidBinding (object):
> """Context object for fluid bindings."""
> def __init__(me, fluid, value):
> me.fluid = fluid
> me.value = value
> def __enter__(me):
> me.fluid._bind(me.value)
> def __exit__(me, ty, val, tb):
> me.fluid._unbind()
>
> class Fluid (object):
> """
> Represents a fluid variable, i.e., one whose binding respects
> the dynamic context rather than the lexical context.
>
> Read and write the Fluid through the `value' property.
>
> The global value is shared by all threads. To dynamically
> bind the fluid, use the context manager `binding':
>
> with myfluid.binding(NEWVALUE):
> ...
>
> The binding is visible in functions called MAP within the
> context body, but not in other threads.
> """
>
> _TLS = T.local()
> _UNBOUND = ['fluid unbound']
> _OMIT = ['fluid omitted']
>
> def __init__(me, value = _UNBOUND):
> """
> Iinitialze a fluid, optionally setting the global value.
> """
> me._value = value
>
> @property
> def value(me):
> """
> Return the current value of the fluid.
>
> Raises AttributeError if the fluid is currently unbound.
> """
> try:
> value, _ = me._TLS.map[me]
> except (AttributeError, KeyError):
> value = me._value
> if value == me._UNBOUND:
> raise AttributeError, 'unbound fluid'
> return value
> @value.setter
> def value(me, value):
> try:
> map = me._TLS.map
> _, stack = map[me]
> map[me] = value, stack
> except (AttributeError, KeyError):
> me._value = value
> @value.deleter
> def value(me):
> me.value = me._UNBOUND
>
> def binding(me, value = _OMIT, unbound = False):
> """
> Bind the fluid dynamically.
>
> If UNBOUND is true then make the fluid be `unbound', i.e.,
> not associated with a value. Otherwise, if VALUE is unset,
> then preserve the current value. Otherwise, set it to
> VALUE.
>
> The fluid can be modified and deleted. This will not affect
> the value outside of the dynamic extent of the context
> (e.g., in other threads, or when the context is unwound).
> """
> if unbound:
> value = me._UNBOUND
> elif value == me._OMIT:
> value = me.value
> return _FluidBinding(me, value)
>
> def _bind(me, value):
> try:
> map = me._TLS.map
> except AttributeError:
> me._TLS.map = map = W.WeakKeyDictionary()
> try:
> old, stack = map[me]
> stack.append(old)
> map[me] = value, stack
> except KeyError:
> map[me] = value, []
>
> def _unbind(me):
> map = me._TLS.map
> _, stack = map[me]
> if stack:
> map[me] = stack.pop(), stack
> else:
> del map[me]
>
> Now we can say
>
> with fluid.binding(new):
> ...
>
> and all is well.
>
> So, how do we piece all of this together to make a resumable exception
> system?
>
> We're going to need to keep a list of handlers. We're going to be
> adding and removing stuff a lot; and we want to make use of the fluid
> mechanism we've already built, which will restore old values
> automatically when we leave a dynamic context. So maintaining a linked
> list seems like a good idea. The nodes in the list will look somewhat
> like this.
>
> class Link (object):
> def __init__(me, item, next):
> me.item = item
> me.next = next
>
> Our handlers are going to be simple functions which take exception
> objects as arguments. A more advanced handler might filter exceptions
> based on their classes. That's not especially difficult to do badly,
> but it's fiddly to do well and it doesn't shed much light on the overall
> mechanism, so I'll omit that complication.
>
> We'll want a fluid for the handler list.
>
> HANDLERS = Fluid(None)
>
> Now we want to run a chunk of code with a handler attached. This seems
> like another good use for a context manager.
>
> class handler (object):
> def __init__(me, func):
> me._func = func
> def __enter__(me):
> me._bind = FluidBinding(HANDLERS,
> Link(me.func, HANDLERS.value)
> me._bind.__enter__()
> def __exit__(me, ty, val, tb):
> return me._bind.__exit__(ty, val, tb)
>
> (Context managers don't compose very nicely. It'd be prettier with the
> contextmanager decorator.)
>
> Let's say that we `signal' resumable exceptions rather than `raising'
> them. How do we do that?
>
> def signal(exc):
> with HANDLERS.binding():
> while HANDLERS.value:
> h = HANDLERS.value
> HANDLERS.value = h.next
> h.item(exc)
>
> Yes, if all of the handlers decline, we just return. This is Bad for
> errors, but good for other kinds of situations, so `signal' is a
> convenient substrate to build on.
>
> def error(exc):
> signal(exc)
> raise RuntimeError, 'unhandled resumable exception'
>
> def warning(exc):
> signal(exc)
> ## Crank up python's usual warning stuff
>
> Note also that handlers are invoked in a dynamic environment which
> doesn't include them or any handlers added since. Obviously they can
> install their own handlers just fine.
>
> Cool. Now how about recovery? This is where nonlocal transfer comes
> in. If a handler wants to take responsibility for the exception, it has
> to make a nonlocal transfer. Where should it go? Let's maintain a
> table of restart points. Again, it'll be a linked list.
>
> RESTARTS = Fluid(None)
>
> class restart (block):
> def __init__(me, name):
> me.name = name
> super(restart, me).__init__(me)
> def invoke(me, value = None):
> me._escape(value)
> def __enter__(me):
> me._bind = FluidBinding(RESTARTS, Link(me, RESTARTS.value))
> me._bind.__enter__()
> return super(restart, me).__enter__()
> def __exit__(me, ty, val, tb):
> ## Poor man's PROG1.
> try:
> return super(restart, me).__exit__(ty, val, tb)
> finally:
> me._bind.__exit__(ty, val, tb)
>
> def find_restart(name):
> r = RESTARTS.value
> while r:
> if r.item.name == name:
> return r.item
> r = r.next
> return None
>
> Using all of this is rather cumbersome, and Python doesn't allow
> syntactic abstraction so there isn't really much we can do to sweeten
> the pill. But I ought to provide an example of this machinery in
> action.
>
> def toy(x, y):
> r = restart('use-value')
> with r:
> if y == 0:
> error(ZeroDivisionError())
> r.result = x/y
> return r.result
>
> def example():
> def zd(exc):
> if not isinstance(exc, ZeroDivisionError):
> return
> r = find_restart('use-value')
> if not r: return
> r.invoke(42)
> print toy(5, 2)
> with handler(zd):
> print toy(1, 0)
>
> Does any of that help?
You could do that.
Or, you could just put your try...finally inside a loop.
Carl Banks
More information about the Python-list
mailing list