A real limitation of contextlib.nested()
I missed the discussion about potentially adding syntactic support for multiple context managers in the with statement, but figured I should mention a real limitation of contextlib.nested that *would* be fixed by adding dedicated syntactic support. There's a genuine semantic difference between this: with cmA(): with cmB(): # whatever and this: with nested(cmA(), cmB()): # whatever The latter is actually more accurately translated as: mgr1, mgr2 = cmA(), cmB(): with mgr1: with mgr2: # whatever That is, when using nested() the later context managers are created outside the scope of the earlier context managers. So, to use Christian's example from the previous discussion: with lock: with open(infile) as fin: with open(outfile, 'w') as fout: fout.write(fin.read()) Using contextlib.nested for that would be outright broken: with nested(lock, open(infile), open(outfile) as (_, fin, fout): fout.write(fin.read()) 1. The files are opened without acquiring the lock first 2. If an IOError is raised while opening "outfile", then "infile" doesn't get closed immediately I created issue 5491 [1] to point out that the contextlib.nested docs could do with being tweaked to make this limitation clearer. Dedicated syntax (such as the form that Christian proposed) would fix this problem: with lock, (open(infile) as fin), (open(outfile, 'w') as fout): fout.write(fin.read()) Of course, a custom context manager doesn't suffer any problems either: @contextmanager def synced_io(lock, infile, outfile): with lock: with open(infile) as fin: with open(outfile) as fout: yield fin, fout with synced_io(lock, infile, outfile) as (fin, fout): fout.write(fin.read()) Cheers, Nick. [1] http://bugs.python.org/issue5491 -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia ---------------------------------------------------------------
On Sat, Mar 14, 2009 at 9:58 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
I missed the discussion about potentially adding syntactic support for multiple context managers in the with statement, but figured I should mention a real limitation of contextlib.nested that *would* be fixed by adding dedicated syntactic support.
There's a genuine semantic difference between this:
with cmA(): with cmB(): # whatever
and this:
with nested(cmA(), cmB()): # whatever
The latter is actually more accurately translated as:
mgr1, mgr2 = cmA(), cmB(): with mgr1: with mgr2: # whatever
That is, when using nested() the later context managers are created outside the scope of the earlier context managers.
So, to use Christian's example from the previous discussion:
with lock: with open(infile) as fin: with open(outfile, 'w') as fout: fout.write(fin.read())
Using contextlib.nested for that would be outright broken:
with nested(lock, open(infile), open(outfile) as (_, fin, fout): fout.write(fin.read())
1. The files are opened without acquiring the lock first 2. If an IOError is raised while opening "outfile", then "infile" doesn't get closed immediately
I created issue 5491 [1] to point out that the contextlib.nested docs could do with being tweaked to make this limitation clearer.
Dedicated syntax (such as the form that Christian proposed) would fix this problem:
with lock, (open(infile) as fin), (open(outfile, 'w') as fout): fout.write(fin.read())
Of course, a custom context manager doesn't suffer any problems either:
@contextmanager def synced_io(lock, infile, outfile): with lock: with open(infile) as fin: with open(outfile) as fout: yield fin, fout
with synced_io(lock, infile, outfile) as (fin, fout): fout.write(fin.read())
fwiw, I believe you could write a version of nested that generates the above code based on the parameters but I believe it'd be disgustingly slow... @contextmanager def slow_nested(*args): code_lines = [] vars = [] code_lines.append('@contextmanager') code_lines.append('def _nested(*args):') for idx in xrange(len(args)): vars.append('c%d' % idx) code_lines.append('%swith args[%d] as %s:' % (' '*(idx+1), idx, vars[-1])) code_lines.append('%syield %s' % (' '*(len(args)+1), ','.join(vars))) code = '\n'.join(code_lines) print 'CODE:\n', code exec(code) yield _nested(*args)
Cheers, Nick.
[1] http://bugs.python.org/issue5491
-- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia --------------------------------------------------------------- _______________________________________________ Python-ideas mailing list Python-ideas@python.org http://mail.python.org/mailman/listinfo/python-ideas
fwiw, I believe you could write a version of nested that generates the above code based on the parameters but I believe it'd be disgustingly slow...
@contextmanager def slow_nested(*args): code_lines = [] vars = [] code_lines.append('@contextmanager') code_lines.append('def _nested(*args):') for idx in xrange(len(args)): vars.append('c%d' % idx) code_lines.append('%swith args[%d] as %s:' % (' '*(idx+1), idx, vars[-1])) code_lines.append('%syield %s' % (' '*(len(args)+1), ','.join(vars))) code = '\n'.join(code_lines) print 'CODE:\n', code exec(code) yield _nested(*args)
Unfortunately, that doesn't help: the problem is the fact that the arguments to nested() are evaluated *before* the call to nested() itself. A version with lazily evaluated arguments (i.e. accepting zero-argument callables that create the context managers instead of accepting the context managers themselves) could do the trick, but that then becomes enough of a pain to use that it wouldn't offer much benefit over writing a dedicated context manager. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia ---------------------------------------------------------------
Oh, your right! +1 for adding the new syntax. Nick Coghlan wrote:
I missed the discussion about potentially adding syntactic support for multiple context managers in the with statement, but figured I should mention a real limitation of contextlib.nested that *would* be fixed by adding dedicated syntactic support.
There's a genuine semantic difference between this:
with cmA(): with cmB(): # whatever
and this:
with nested(cmA(), cmB()): # whatever ...
Nick Coghlan wrote:
Dedicated syntax (such as the form that Christian proposed) would fix this problem:
with lock, (open(infile) as fin), (open(outfile, 'w') as fout): fout.write(fin.read())
Could 'and' possibly be used sense it is a flow control operator in python. with lock and open(infile) as fin and open(outfile, 'w' as fout: fout.write(fin.read()) Ron
Ron Adam schrieb:
Nick Coghlan wrote:
Dedicated syntax (such as the form that Christian proposed) would fix this problem:
with lock, (open(infile) as fin), (open(outfile, 'w') as fout): fout.write(fin.read())
Could 'and' possibly be used sense it is a flow control operator in python.
with lock and open(infile) as fin and open(outfile, 'w' as fout: fout.write(fin.read())
But it isn't a control flow operator. It is a boolean operator, and since "with" expressions are expressions, it's perfectly valid there. Georg -- Thus spake the Lord: Thou shalt indent with four spaces. No more, no less. Four shall be the number of spaces thou shalt indent, and the number of thy indenting shall be four. Eight shalt thou not indent, nor either indent thou two, excepting that thou then proceed to four. Tabs are right out.
participants (5)
-
Georg Brandl
-
Gregory P. Smith
-
Mathias Panzenböck
-
Nick Coghlan
-
Ron Adam