On Mon, Aug 8, 2016 at 11:59 PM, Chris Angelico <ros...@gmail.com> wrote:

> On Tue, Aug 9, 2016 at 7:14 AM, Wolfgang Maier
> <wolfgang.ma...@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
> _______________________________________________
> Python-Dev mailing list
> Python-Dev@python.org
> 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
Python-Dev@python.org
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