[Python-ideas] ScopeGuardStatement/Defer Proposal

Manuel Barkhau mbarkhau at googlemail.com
Fri Feb 17 01:06:52 CET 2012


Hi everybody,

I'd like to suggest adopting something similar to the
ScopeGuardStatement from the D programming language. A description of
the D version can be found here:
http://d.digitalmars.com/2.0/statement.html#ScopeGuardStatement

It is also similar to golangs "defer" statement:
http://golang.org/doc/go_spec.html#Defer_statements

So these are roughly equivelent:
defer {block} // in golang
scope(exit) {block} // in D

I have written a context manager that approximates the behavior of the
scope statement in D: http://ideone.com/vNmq8
The use of lambdas or nested functions doesn't look very nice however.


So, on to the proposal.

I think the "defer" keyword is more appropriate than "scope" and the
function like syntax of "scope(exit)" doesn't fit with the overall
python syntax.

There are three ways to define a defer block.
 - "defer: BLOCK", which is the same as "defer {BLOCK} in golang or
   "scope(exit) {BLOCK}" in D.
 - "defer EXPR as VAR: BLOCK", which is similar to "scope(failure)". It
   differs in that it specifies the exception that caused the failure
   and is only called for matching exceptions.
 - "defer EXPR: BLOCK else: BLOCK", where the else BLOCK is executed
   when no exception occurs. This is similar to "scope(success)" and the
   existing except: else: construct in python.

As "defer:" is currently invalid syntax, there shouldn't be any code
breakage from adding the new keyword.

Some rules:
 - Deferred blocks are executed in the reverse lexical order in which
   they appear.
 - If a function returns before reaching a defer statement, it will not
   be executed.
 - If a defer block raises an error, a lexically earlier defer block may
   catch it.
 - If multiple defer blocks raise errors or return results, the raise or
   return of the lexically earlier defer will mask the previous result
   or error.


Some example code:

>>> def ordering_example():
...     print(1)
...     defer: print(2)
...     defer: print(3)
...     print(4)
...
>>> ordering_example()
1
4
3
2

Handling exceptions:

>>> def defer_example():
...     # setup
...     defer:                  # always executed
...         # cleanup
...     defer Exception as e:   # executed if exception is raised
...         # handle exception
...     else:                   # executed if no exception is raised
...         # success code
...
...     # your usual code
...     # possibly raise exception


Equivalent using try/except/finally:

>>> def try_example():
...     # setup
...     try:
...         # your usual code
...         # possibly raise exception
...     except Exception as e:
...         # handle exception
...     else:
...         # success code
...     finally:
...         # cleanup


The nesting advantage becomes more apparent when more are required. Here
is an example from
http://www.doughellmann.com/articles/how-tos/python-exception-handling/index.html

    #!/usr/bin/env python

    import sys
    import traceback

    def throws():
        raise RuntimeError('error from throws')

    def cleanup():
        raise RuntimeError('error from cleanup')

    def nested():
        try:
            throws()
        except Exception as original_error:
            try:
                raise
            finally:
                try:
                    cleanup()
                except:
                    pass # ignore errors in cleanup

    def main():
        try:
            nested()
            return 0
        except Exception as err:
            traceback.print_exc()
            return 1

    if __name__ == '__main__':
        sys.exit(main())


Here are the equivalent of main and nested functions using defer:

    def nested():
        defer RuntimeError: pass # ignore errors in cleanup
        defer: cleanup()
        throws()

    def main():
        defer Exception as err:
            traceback.print_exc()
            return 1
        else:
            return 0

        nested()

Notice that we don't even need "defer Exception as original_error:
raise" after "defer: cleanup()" in order to preserve the stack trace. It
will go up the call stack, so long as no defer handles it or masks it
with another exception.


This proposal would probably have had a better chance before the
introduction of the "with" statement, but I still think it may be useful
in cases where you don't want to write a context manager. Context
managers may also not have access to the scope they are used in, which
may be inconvenient in some cases.

For code where try/except/finally would otherwise be required, I think
the advantages make this proposal at least worth considering. You don't
need to nest your normal code in a try block and you can place error
handling code together with relevant sections, rather than further down
in an except block.

I'm sure there is much I have overlooked, possibly this is technically
difficult and of course there is the minor task of implementation. But
other than that what do you think?


Manuel



More information about the Python-ideas mailing list