Home > Software design >  Invoking a constructor in a 'with' statement
Invoking a constructor in a 'with' statement

Time:09-22

I have the following code:

class Test:

    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print(f'entering {self.name}')

    def __exit__(self, exctype, excinst, exctb) -> bool:
        print(f'exiting {self.name}')
        return True

with Test('first') as test:
    print(f'in {test.name}')

test = Test('second')
with test:
    print(f'in {test.name}')

Running it produces the following output:

entering first
exiting first
entering second
in second
exiting second

But I expected it to produce:

entering first
in first
exiting first
entering second
in second
exiting second

Why isn't the code within my first example called?

CodePudding user response:

The __enter__ method should return the context object. with ... as ... uses the return value of __enter__ to determine what object to give you. Since your __enter__ returns nothing, it implicitly returns None, so test is None.

with Test('first') as test:
    print(f'in {test.name}')

test = Test('second')
with test:
    print(f'in {test.name}')

So test is none. Then test.name is an error. That error gets raised, so Test('first').__exit__ gets called. __exit__ returns True, which indicates that the error has been handled (essentially, that your __exit__ is acting like an except block), so the code continues after the first with block, since you told Python everything was fine.

Consider

def __enter__(self):
    print(f'entering {self.name}')
    return self

You might also consider not returning True from __exit__ unless you truly intend to unconditionally suppress all errors in the block (and fully understand the consequences of suppressing other programmers' errors, as well as KeyboardInterrupt, StopIteration, and various system signals)

CodePudding user response:

The problem is that your __enter__ method returns None. Hence, test is assigned None.

Then you try to access (None).name, which raises an error. Since your __exit__ method returns True always, it will suppress any errors. According to the docs:

Returning a true value from this method will cause the with statement to suppress the exception and continue execution with the statement immediately following the with statement.

CodePudding user response:

I believe this behavior is because __enter__ must return something that will be operated on, that in this case will be accessed with the name test. By changing __enter__ to the following

def __enter__(self):
    print(f"entering {self.name}")
    return self

we get the expected behavior.

CodePudding user response:

The reason is that the first and second cases don't do the same.

In first:

  • the object is created, which calls __init__;
  • then with calls __enter__;
  • then as stores the result of __enter__ into test
  • since __enter__ doesn't have a return value, test is None.

In second:

  • the object is created, which calls __init__;
  • then assigned to test;
  • then with calls __enter__;
  • but nothing is done with the result of __enter__;
  • so test keeps referring to the object that was created originally.

In both cases, __exit__ is called for the object that with is handling, so you see the right label being printed; it's just that, in first, the test identifier is not bound to that same object.

NB __enter__ doesn't have to return self. It might return something else entirely, for instance you may open a file and make __enter__ return the stream, while __exit__ may close it. If it were a given that __enter__ should return self, that would be redundant and could just be implied.

CodePudding user response:

Explanation:

__enter__ is giving None as an output, since there is no return, therefore it would directly trigger __exit__ since None has no attribute name, ex:

>>> None.name
Traceback (most recent call last):
  File "<pyshell#0>", line 1, in <module>
    None.__name__
AttributeError: 'NoneType' object has no attribute 'name'
>>> 

If you set it to call __class__.__name__ (None objects have that attribute, which gives NoneType), you could find the problem easily:

class Test:

    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print(f'entering {self.name}')

    def __exit__(self, exctype, excinst, exctb) -> bool:
        print(f'exiting {self.name}')
        return True

with Test('first') as test:
    print(f'in {test.__class__.__name__}')

test = Test('second')
with test:
    print(f'in {test.__class__.__name__}')

Output:

entering first
in NoneType
exiting first
entering second
in Test
exiting second

As you can see, it says in NoneType, not returning any value is the reason to this. In a lot of cases, __enter__ doesn't need to return, but in this case the Test class needs it to return.

Solution:

The solution would be to keep the Test instance, so that it calls the name of a returned self after the context manager __enter__ result. So far the __enter__ results None, therefore None.name attribute doesn't exist. So if you return self, test.name attribute would exist.

The solution would be to return self in the __enter__ magic method implementation:

    ...
    def __enter__(self):
        print(f'entering {self.name}')
        return self
    ...

Full code:

class Test:

    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print(f'entering {self.name}')
        return self

    def __exit__(self, exctype, excinst, exctb) -> bool:
        print(f'exiting {self.name}')
        return True

with Test('first') as test:
    print(f'in {test.name}')

test = Test('second')
with test:
    print(f'in {test.name}')

Output:

entering first
in first
exiting first
entering second
in second
exiting second

The extra info I gave that the other answers didn't give is a more concrete proof of the __enter__ method implementation giving None. I showed an example as well of it.

CodePudding user response:

A with statement does not create a scope (like if, for and while do not create a scope either).

As a result, Python will analyze the code and see that you made an assignment in the with statement, and thus that will make the variable local (to the real scope).

In Python variables do not need initialization in all code paths: as a programmer, you are responsible to make sure that a variable is assigned before it is used. This can result in shorter code: say for instance you know for sure that a list contains at least one element, then you can assign in a for loop. In Java assignment in a for loop is not considered safe (since it is possible that the body of the loop is never executed).

Initialization before the with scope can be safer in the sense that after the with statement we can safely assume that the variable exists. If on the other hand the variable should be assigned in the with statement, not initializing it before the with statement actually results in an additional check: Python will error if somehow the assignment was skipped in the with statement.

A with statement is only used for context management purposes. It forces (by syntax) that the context you open in the with is closed at the end of the indentation.

  • Related