[Python-Dev] PEP 463: Exception-catching expressions

Victor Stinner victor.stinner at gmail.com
Sat Feb 22 03:07:17 CET 2014


Hi,


2014-02-21 4:15 GMT+01:00 Chris Angelico <rosuav at gmail.com>:
> PEP: 463
> Title: Exception-catching expressions

Nice PEP. Good luck, it's really hard to modify the language. Be
prepared to get many alternatives, criticisms, and suggestions. Good
luck to handle them :-) Here is mine.

I like the simple case "expr1 except Exception: expr2", but it is used
twice like "expr1 except Exception: (expr2 except Exception: expr3)".

> * list.pop() - no way to return a default

I never used this once, I didn't know that it exists :)

> * min(sequence, default=default) - keyword argument in place of
>   ValueError
> * sum(sequence, start=default) - slightly different but can do the
>   same job

You should remove the sum() from your list, the start parameter is
unrelated. It is used to control input and output types for example.
It's not related to an exception.

> The proposal adds this::
>
>     lst = [1, 2]
>     value = lst[2] except IndexError: "No value"

The PEP looks nice on such simple example...

> Consider this example of a two-level cache::
>     for key in sequence:
>         x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key)))
>         # do something with x

... but I don't like when it is used to build complex expressions.

At the first read, I'm unable to understand this long expression. At
the second read, I'm still unable to see which instruction will be
executed first: lvl1[key] or lvl2[key]?

The advantage of the current syntax is that the control flow is
obvious, from the top to the bottom:

# start
try:
    x = lvl1[key]   # first instruction
except KeyError:
    try:
        x = lvl2[key]
    except KeyError:
        x = f(key)   # latest instruction
# end

Thanks to Python indentation, it's easier to see the control flow.

After having read a lot of code last 10 years, I now try to follow
this rule: one instruction per line.

For example, I don't like "if ((res = func()) == NULL)" in the C
language, I always split it into two lines. A drawback of writing more
than one instruction is that it's hard to debug instruction per
instruction (in pdb/gdb). Think for example of "if ((res = func()) ==
NULL)": if you execute step by step, how do you know which instruction
is currently executed?

You should maybe write your example differently:

x = (lvl1[key]
        except KeyError: (lvl2[key]
                                       except KeyError: f(key)))

It looks like the classic try/except syntax.

>         # sys.abiflags may not be defined on all platforms.
>         _CONFIG_VARS['abiflags'] = sys.abiflags except AttributeError: ''
>
>         # Lib/sysconfig.py:529:
>         try:
>             _CONFIG_VARS['abiflags'] = sys.abiflags
>         except AttributeError:
>             # sys.abiflags may not be defined on all platforms.
>             _CONFIG_VARS['abiflags'] = ''

getattr(sys, 'abiflags', '') can be used here.

> Retrieve an indexed item, defaulting to None (similar to dict.get)::
>     def getNamedItem(self, name):
>         return self._attrs[name] except KeyError: None
>
>     # Lib/xml/dom/minidom.py:573:
>     def getNamedItem(self, name):
>         try:
>             return self._attrs[name]
>         except KeyError:
>             return None

Hum, I don't understand this one. name is an index or a key? If it's a
key and self._attrs is a dict, you can use self._attrs.get(name).

> Translate numbers to names, falling back on the numbers::
>             g = grp.getgrnam(tarinfo.gname)[2] except KeyError: tarinfo.gid
>             u = pwd.getpwnam(tarinfo.uname)[2] except KeyError: tarinfo.uid
>
>             # Lib/tarfile.py:2198:
>             try:
>                 g = grp.getgrnam(tarinfo.gname)[2]
>             except KeyError:
>                 g = tarinfo.gid
>             try:
>                 u = pwd.getpwnam(tarinfo.uname)[2]
>             except KeyError:
>                 u = tarinfo.uid

Note (for myself): the KeyError comes from the function call, not from entry[2].

> Retrieving a message from either a cache or the internet, with auth
> check::
>
>     logging.info("Message shown to user: %s",((cache[k]
>         except LookupError:
>             (backend.read(k) except OSError: 'Resource not available')
>         )
>         if check_permission(k) else 'Access denied'
>     ) except BaseException: "This is like a bare except clause")

Oh, again: I prefer the try/except syntax (above), it's more easy to
see the control flow and understand the code. Too many instruction per
lines, the code is too "dense".

It looks like cache[k] is the first executed instruction, but it's
wrong: check_permission(k) is executed before.

So I prefer this version:

>     try:
>         if check_permission(k):
>             try:
>                 _ = cache[k]
>             except LookupError:
>                 try:
>                     _ = backend.read(k)
>                 except OSError:
>                     _ = 'Resource not available'
>         else:
>             _ = 'Access denied'
>     except BaseException:
>         _ = "This is like a bare except clause"
>     logging.info("Message shown to user: %s", _)

(...)

> Looking up objects in a sparse list of overrides::
>
>     (overrides[x] or default except IndexError: default).ping()
>
>     try:
>         (overrides[x] or default).ping()
>     except IndexError:
>         default.ping()

The try/except version should use a variable and call the ping()
method outside the try/except block, to ensure that the IndexError
comes from overrides[x], not from the ping() method.

Victor


More information about the Python-Dev mailing list