Home > Net >  Should the "opening work" of a context manager happen in __init__ or __enter__?
Should the "opening work" of a context manager happen in __init__ or __enter__?

Time:01-01

I found the following example of a Context Manager for a File object:

class File(object):
    def __init__(self, file_name, method):
        self.file_obj = open(file_name, method)
    def __enter__(self):
        return self.file_obj
    def __exit__(self, type, value, traceback):
        self.file_obj.close()

Here, the work done by the manager, that is actually opening the file, happens in the __init__ method. However, in the accompanying text, they suggest that the file opening should happen in the __enter__ call:

Let’s talk about what happens under-the-hood.

  1. The with statement stores the exit method of the File class.
  2. It calls the enter method of the File class.
  3. The enter method opens the file and returns it.
  4. The opened file handle is passed to opened_file.
  5. We write to the file using .write().
  6. The with statement calls the stored exit method.
  7. The exit method closes the file.

Which is the correct approach in general? It seems to be that the work undone by __exit__ should happen in __enter__, not __init__ since those are paired 1:1 by the context manager mechanism, but this example leaves me doubtful.

CodePudding user response:

There is no general answer. It depends on what the work is. For example, for a file, opening happens in __init__, but for a lock, locking happens in __enter__.

One important thing to think about is, what should happen if the object is not used as a context manager, or not immediately used as a context manager? What should the object's state be after construction? Should relevant resources already be acquired at that point?

For a file, the answer is yes, the file should already be open, so opening happens in __init__. For a lock, the answer is no, the lock should not be locked, so locking goes in __enter__.

Another thing to consider is, should this object be usable as a context manager more than once? If entering a context manager twice should do a thing twice, that thing needs to happen in __enter__.

CodePudding user response:

Hereis a better example

class TraceBlock:

    def message(self, arg):
        print('running '   arg)
    
    def __enter__(self):
        print('starting with block')
        return self

    def __exit__(self, exc_type, exc_value, exc_tb):
        if exc_type is None:
            print('exited normally\n')
        else:
            print('raise an exception! '   str(exc_type))
            return False # Propagate

#--------------------------

if __name__ == '__main__':

    with TraceBlock() as action:

        action.message('test 1')
        print('reached')
    
    with TraceBlock() as action:

        action.message('test 2')
        raise TypeError
        print('not reached')

If The Exit returns False the exception will pass on to other handler, if it return True the exception would not go to others.

  • Related