[Python-Dev] Rewrite @contextlib.contextmanager in C
Chris Angelico
rosuav at gmail.com
Mon Aug 8 17:59:39 EDT 2016
On Tue, Aug 9, 2016 at 7:14 AM, Wolfgang Maier
<wolfgang.maier at biologie.uni-freiburg.de> wrote:
> Right, I think a fairer comparison would be to:
>
> class ctx2:
> def __enter__(self):
> self.it = iter(self)
> return next(self.it)
>
> def __exit__(self, *args):
> try:
> next(self.it)
> except StopIteration:
> pass
>
> def __iter__(self):
> yield
>
> With this change alone the slowdown diminishes to ~ 1.7x for me. The rest is
> probably the extra overhead for being able to pass exceptions raised inside
> the with block back into the generator and such.
I played around with a few other variants to see where the slowdown
is. They all work out pretty much the same as the above; my two
examples are both used the same way as contextlib.contextmanager is,
but are restrictive on what you can do.
import timeit
import contextlib
import functools
class ctx1:
def __enter__(self):
pass
def __exit__(self, *args):
pass
@contextlib.contextmanager
def ctx2():
yield
class SimplerContextManager:
"""Like contextlib._GeneratorContextManager but way simpler.
* Doesn't reinstantiate itself - just reinvokes the generator
* Doesn't allow yielded objects (returns self)
* Lacks a lot of error checking. USE ONLY AS DIRECTED.
"""
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
def __call__(self, *a, **kw):
self.gen = self.func(*a, **kw)
return self
def __enter__(self):
next(self.gen)
return self
def __exit__(self, type, value, traceback):
if type is None:
try: next(self.gen)
except StopIteration: return
else: raise RuntimeError("generator didn't stop")
try: self.gen.throw(type, value, traceback)
except StopIteration: return True
# Assume any instance of the same exception type is a proper reraise
# This is way simpler than contextmanager normally does, and costs us
# the ability to detect exception handlers that coincidentally raise
# the same type of error (eg "except ZeroDivisionError: print(1/0)").
except type: return False
# Check that it actually behaves correctly
@SimplerContextManager
def ctxdemo():
print("Before yield")
try:
yield 123
except ZeroDivisionError:
print("Absorbing 1/0")
return
finally:
print("Finalizing")
print("After yield (no exception)")
with ctxdemo() as val:
print("1/0 =", 1/0)
with ctxdemo() as val:
print("1/1 =", 1/1)
#with ctxdemo() as val:
# print("1/q =", 1/q)
@SimplerContextManager
def ctx3():
yield
class TooSimpleContextManager:
"""Now this time you've gone too far."""
def __init__(self, func):
self.func = func
def __call__(self):
self.gen = self.func()
return self
def __enter__(self):
next(self.gen)
def __exit__(self, type, value, traceback):
try: next(self.gen)
except StopIteration: pass
@TooSimpleContextManager
def ctx4():
yield
class ctx5:
def __enter__(self):
self.it = iter(self)
return next(self.it)
def __exit__(self, *args):
try:
next(self.it)
except StopIteration:
pass
def __iter__(self):
yield
t1 = timeit.timeit("with ctx1(): pass", setup="from __main__ import ctx1")
print("%.3f secs" % t1)
for i in range(2, 6):
t2 = timeit.timeit("with ctx2(): pass", setup="from __main__
import ctx%d as ctx2"%i)
print("%.3f secs" % t2)
print("slowdown: -%.2fx" % (t2 / t1))
My numbers are:
0.320 secs
1.354 secs
slowdown: -4.23x
0.899 secs
slowdown: -2.81x
0.831 secs
slowdown: -2.60x
0.868 secs
slowdown: -2.71x
So compared to the tight custom-written context manager class, all the
"pass it a generator function" varieties look pretty much the same.
The existing contextmanager factory has several levels of indirection,
and that's probably where the rest of the performance difference comes
from, but there is some cost to the simplicity of the gen-func
approach.
My guess is that a C-implemented version could replace piles of
error-handling code with simple pointer comparisons (hence my
elimination of it), and may or may not be able to remove some of the
indirection. I'd say it'd land about in the same range as the other
examples here. Is that worth it?
ChrisA
More information about the Python-Dev
mailing list