Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Yielding from within with statement and __exit__ method of context manager

Consider following Python 2.x code snippet.

from __future__ import print_function


class myfile(file):
    def __exit__(self, *excinfo):
        print("__exit__ called")
        super(myfile, self).__exit__(*excinfo)


def my_generator(file_name):
    with myfile(file_name) as fh:
        for line in fh:
            yield line.strip()


gen = my_generator('file.txt')
print(next(gen))
print("Before del")
del gen
print("After del")

Output of this script (given file.txt has more than one line) is:

Line 1 from file
Before del
__exit__ called
After del

I'm interested about __exit__ call specifically.

What triggers execution of his method? For what we know, code never left with statement (it "stopped" after yield statement and never continued). Is it guaranteed that __exit__ will be called when reference count of generator drops to 0?

like image 760
Łukasz Rogalski Avatar asked May 16 '17 15:05

Łukasz Rogalski


People also ask

What does yield do in context manager?

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.

What should __ exit __ return?

__enter__() is provided which returns self while object. __exit__() is an abstract method which by default returns None .

What does __ exit __ do in Python?

__exit__() method The __exit__ method takes care of releasing the resources occupied with the current code snippet. This method must be executed no matter what after we are done with the resources.

Is __ exit __ always called?

__exit__() automatically gets called and closes the file for you, even if an exception is raised inside the with block.


2 Answers

On reclamation of a generator object, Python calls its close method, raising a GeneratorExit exception at the point of its last yield if it wasn't already finished executing. As this GeneratorExit propagates, it triggers the __exit__ method of the context manager you used.

This was introduced in Python 2.5, in the same PEP as send and yield expressions. Before then, you couldn't yield inside a try with a finally, and if with statements had existed pre-2.5, you wouldn't have been able to yield inside one either.

like image 169
user2357112 supports Monica Avatar answered Jan 04 '23 05:01

user2357112 supports Monica


To add to @user2357112's answer, the with block breaks when an exception is raised inside of it. This exception is passed to the __exit__ method of the object that was created for the context.

The file class seems to pass silently the GeneratorExit exception, since nothing signals it. However, if you print argc in your myfile.__exit__ method, you will see that the context was not closed naturally:

class myfile(file):
    def __exit__(self, *excinfo):
        print("__exit__ called")
        print(excinfo[0]) # Print the reason why the context exited
        super(myfile, self).__exit__(*excinfo)

Output of your script:

Line 1 from file
Before del
__exit__ called
<type 'exceptions.GeneratorExit'>
After del
like image 42
Right leg Avatar answered Jan 04 '23 05:01

Right leg