52
import contextlib
import time

@contextlib.contextmanager
def time_print(task_name):
    t = time.time()
    try:
        yield
    finally:
        print task_name, "took", time.time() - t, "seconds."


def doproc():
    x=1+1


with time_print("processes"):
    [doproc() for _ in range(500)]

# processes took 15.236166954 seconds.

when does doproc get executed when using this decorator?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Shuman
  • 3,914
  • 8
  • 42
  • 65
  • [From the docs:](https://docs.python.org/2/library/contextlib.html#contextlib.contextmanager) "At the point where the generator yields, the block nested in the with statement is executed. The generator is then resumed after the block is exited. If an unhandled exception occurs in the block, it is reraised inside the generator at the point where the yield occurred." – Two-Bit Alchemist Feb 18 '16 at 18:45
  • 3
    `yield` without argument is semantically equivalent to `yield None` – Sebastian Hoffmann Feb 18 '16 at 18:46

2 Answers2

64

yield expression returns control to the whatever is using the generator. The generator pauses at this point, which means that the @contextmanager decorator knows that the code is done with the setup part.

In other words, everything you want to do in the context manager __enter__ phase has to take place before the yield.

Once your context exits (so the block under the with statement is done), the @contextmanager decorator is called for the __exit__ part of the context manager protocol and will do one of two things:

  • If there was no exception, it'll resume your generator. So your generator unpauses at the yield line, and you enter the cleanup phase, the part

  • If there was an exception, the decorator uses generator.throw() to raise that exception in the generator. It'll be as if the yield line caused that exception. Because you have a finally clause, it'll be executed before your generator exits because of the exception.

So, in your specific example the sequence is as follows:

  1. with time_print("processes"):

    This creates the context manager and calls __enter__ on that.

  2. The generator starts execution, t = time.time() is run.

  3. The yieldexpression pauses the generator, control goes back to the decorator. This takes whatever was yielded and returns that to the with statement, in case there is an as target part. Here None is yielded (there is only a plain yield expression).

  4. [doproc() for _ in range(500)] is run and completes.

  5. The context manager __exit__ method is run, no exception is passed in.

  6. The decorator resumes the generator, it continues where it left off.

  7. The finally: block is entered and print task_name, "took", time.time() - t, "seconds." is executed.

  8. The generator exits, the decorator __exit__ method exits, all is done.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • I do not understand the phrase "This takes whatever was yielded". But `yield` is without parameter. It did not 'yield' anything... – Alex Mar 27 '18 at 06:45
  • 4
    @Alex: you always yield *something*. That can be `None`, just like `return` exits a function and the return value is then `None`. But you can also yield something explicitly, so `yield some_object`, at which point that value can be assigned to a target with the `with context_manager as ` syntax. – Martijn Pieters Mar 27 '18 at 06:50
12

Excellent explanation by @Martijn Pieters. Since the yield is redundant in your case, you can achieve the same by creating your own context manager (without yield and contextlib.contextmanager). This is simpler and more readable. So in your case you can implement something as follows.

import time

class time_print(object):
    def __init__(self, task_name):
        self.task_name = task_name

    def __enter__(self):
        self.t = time.time()

    def __exit__(self):
        print self.task_name, "took", time.time() - self.t, "seconds."

def doproc():
    x = 1 + 1


with time_print("processes"):
    # __enter__ is called
    [doproc() for _ in range(500)]
    # __exit__ is called

Internally contextlib.contextmanager calls these enter and exit magic functions as explained by @Martijn-Pieters. Hope this helps!

djvg
  • 11,722
  • 5
  • 72
  • 103
Manikumar Perla
  • 341
  • 4
  • 10