Dealing with context classes in Python 2.4

2019-07-16 04:17发布

问题:

I'm trying to use the python-daemon module. It supplies the daemon.DaemonContext class to properly daemonize a script. Although I'm primarily targeting Python 2.6+, I'd like to maintain backwards compatibility to version 2.4.

Python 2.5 supports importing contexts from future, but Python 2.4 has no such facility. I figured I could just catch whatever error the with statement raises and enter and exit the context manually for 2.4, but I can't seem to catch the SyntaxError raised.

Is there any way to accomplish this short of explicitly checking the interpreter version? Below is the gist of what I'm trying to do and the problem I'm getting. In Real Life I don't have control of the context class, so it needs to work without mangling the original class, ie not like these ideas.

Nevermind if Python 2.4 can't run python-daemon; I'd at least like to able to catch the error and implement a fallback or something.

Thanks for helping.

#!/usr/bin/python2.4
from __future__ import with_statement
# with_statement isn't in __future__ in 2.4.
# In interactive mode this raises a SyntaxError.
# During normal execution it doesn't, but I wouldn't be able to catch it
# anyways because __future__ imports must be at the beginning of the file, so
# that point is moot.


class contextable(object):
    def __enter__(self):
        print('Entering context.')
        return None
    def __exit__(self, exc_type, exc_val, exc_tb):
        print('Exiting context.')
        return False

def spam():
    print('Within context.')

context = contextable()

try:
    with context: # This raises an uncatchable SyntaxError.
        spam()
except SyntaxError, e: # This is how I would like to work around it.
    context.__enter__()
    try:
        spam()
    finally:
        context.__exit__(None, None, None)

回答1:

SyntaxError is diagnosed by the Python compiler as it compiles -- you're presumably trying to "catch" it from code that's being compiled as part of the same module (e.g., that's what you're doing in your code sample), so of course it won't work -- your "catching" code hasn't been compiled yet (because compilation has terminated unsuccessfully) so it can't catch anything.

You need to ensure the code that might have a syntax error gets compiled later than the catching code -- either put it in a separate module that you import in the try clause, or in a string you compile with the built-in by that name (you can later execute the bytecode resulting from the compile call, if it terminates successfully).

Neither possibility looks good for your purposes, I think. I suspect that using two separate modules (and probably picking between them depending on the "does this compile" check, but a version check sounds much cleaner to me) is the only "clean" solution, unfortunately.

Edit: here's how to microbenchmark try/except against version checks:

$ python2.4 -mtimeit 'try:
  compile("with x: pass", "", "exec")
except SyntaxError: x=1
else: x=2'
100000 loops, best of 3: 10.8 usec per loop
$ python2.6 -mtimeit 'try:
  compile("with x: pass", "", "exec")
except SyntaxError: x=1
else: x=2'
10000 loops, best of 3: 40.5 usec per loop

$ python2.4 -mtimeit -s'import sys' 'if sys.version>="2.5": x=2
else: x=1'
1000000 loops, best of 3: 0.221 usec per loop
$ python2.6 -mtimeit -s'import sys' 'if sys.version>="2.5": x=2
else: x=1'
10000000 loops, best of 3: 0.156 usec per loop

As you see, the version I consider cleaner is 10.8 / 0.221, almost 50 times faster, on 2.4, and 40.5 / 0.156, almost 260 times faster, on 2.6. In general (with rare exceptions), the clean (i.e., "pythonic") approach will turn out to be the better optimized one in Python -- often, at least part of the reason can be that Python core developers focus on facilitating and encouraging the use of constructs they like, rather than that of constructs they dislike.