new syntax for cleanup (was Re: destructors order not guaranteed?)

Alex Martelli aleaxit at yahoo.com
Thu Nov 2 04:55:58 EST 2000


"Jeff Petkau" <jpet at eskimo.com> wrote in message
news:Nk8M5.2535$Vk6.390261 at paloalto-snr1.gtei.net...
    [snip]
> > So, the beautiful C++ idiom...:
> >
> >     template <class Resource>
> >     struct Locker {
> >         Resource& pr;
> >         void (*closer)(Resource&);
> >         Locker(Resource& pr, void (*closer)(Resource&)):
> >             pr(pr), closer(closer) {}
> >         ~Locker() { closer(pr); }
> >     };
> >
> > has no appropriate Python translation (nor Java, etc).
>
> This is the one thing I really miss from C++ when I'm using
> sensible languges.

Ditto.  But it's just a matter of convenience.

> There are lots of things that normally
> get cleaned up in stack unwind order--locks, semaphores,
> open files, fp state, APIs with push/pop semantics like
> OpenGL, etc.--and it's surprisingly difficult to get this
> right in Python.

And in C++, too -- if you want to consider every possible
exception and handle it decently.  Exception-safe code is
NO walkover in any language.  Consider...:

> cases correctly. (Your example illustrates the most common
> form of this bug--if acquire(), getit(), or dropIt() throws,
> cleanup code is either missed or called when it shouldn't be.)

If a function called in a C++ destructor throws, and the
exception exits from the destructor, what happens next is
pandemonium.  "A destructor must not throw" is not written
in the C++ standard, but it might as well have been, given
the disasters caused by destructors that throw.  And having
each destructor completely wrapped by a totally generic
try/catch is hardly a solution -- exceptions are not meant
to be totally ignored in such ways, and there's no telling
how compromised the semantic state of your program might
be if you do that in some library code.

So, in practice, a silent unwritten convention "functions
that release resources (and other cleanup tasks typically
called in destructors) must never find themselves in a
situation in which an exception _must_ be raised" seems
to have arisen (at least in the C++ circles I frequent).

This does have system-level architectural implications,
but it seems they're generally sensible ones (e.g., they
basically forbid the pattern where an object accumulates
'work to be done' indications, and fires it all off on
destruction -- when it's probably too late to do anything
sensible if terrible, exception-requiring anomalies should
emerge; not a good pattern anyway, that is...).

You _are_ entirely right that it was silly of me to
'translate' the C++ snippet
    {
        Locker<dbhandle> lock1(myDatabase.acquire(), dbRelease);
to the Python snippet:
        try: lock1=myDatabase.acquire()
            # ...
        finally: dbRelease(lock1)
because the latter explicity requires the finalization
to take place _if the acquisition throws_, which is when
we most definitely *don't* want it.  So, it should be:
        lock1 = myDatabase.acquire()
        try:
            # ...
        finally: dbRelease(lock1)
i.e., release (no matter what else happens in the meantime)
if and only if the acquisition itself succeeds.

You're also right that this places the finalization
textually far from the initialization, and may engender
excessive nesting due to granularity issues, unless
tempered with other idioms (whether those idioms should
also cover up for exception-throwing finalization ops,
well, that may be harder).  E.g.,
    lock2 = None
    lock1 = myDatabase.acquire()
    try:
        lock2 = myResource.getit()
        fonz()
    finally:
        if not lock2 is None: dropIt(lock2)
        dbRelease(lock1)

This avoids the extra nesting by using lock2 as a
flag of 'has this ever been acquired' (and several
variants are of course possible).  If dropIt throws,
this still has problems (no dbRelease gets called).


Your idea for a 'cleanup' (to be written close to
the initialization, but executed on exit, like a
'finally') is interesting.  I'm not sure a context
dependent keyword makes much sense here, though.

Perhaps one could overload 'finally' (which is
already a keyword), when not paired with 'try',
to mean "do this on exiting the current frame"
rather than "do this on exiting the try-block";
the meaning seems close enough that the overload
could help understanding rather than confuse.

Or maybe finally-on-its-own could mean, as in
your first suggestion for cleanup, "wrap a try
block around all the rest of this block, with
this finally-clause" -- this would restrict the
change to the parser (isolated-finally, rather
than being an error, would generate bytecode
that is already currently valid).  (I'm even
starting to wonder if isolated-except might not
be similarly useful, with a totally similar
interpretation to isolated-finally...)


Alex






More information about the Python-list mailing list