Re: [Python-Dev] Rewrite @contextlib.contextmanager in C

2016年8月09日 11:36:30 -0700

On Mon, Aug 8, 2016 at 11:59 PM, Chris Angelico <[email protected]> wrote:
> On Tue, Aug 9, 2016 at 7:14 AM, Wolfgang Maier
> <[email protected]> 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
> _______________________________________________
> Python-Dev mailing list
> [email protected]
> https://mail.python.org/mailman/listinfo/python-dev
> Unsubscribe: https://mail.python.org/mailman/options/python-dev/g.rodola%
> 40gmail.com
>
Thanks for all this useful info. And I agree, some sort of slowdown should
be expected because it's the price you pay for the additional flexibility
that @contextmanager offers over a "raw" ctx manager class, which will
always be faster as "it does less".
-- 
Giampaolo - http://grodola.blogspot.com
_______________________________________________
Python-Dev mailing list
[email protected]
https://mail.python.org/mailman/listinfo/python-dev
Unsubscribe: 
https://mail.python.org/mailman/options/python-dev/archive%40mail-archive.com

Reply via email to