Invoking a constructor in a 'with' statement
PythonPython 3.xClassWith StatementPython Problem Overview
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?
Python Solutions
Solution 1 - Python
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)
Solution 2 - Python
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.
Solution 3 - Python
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.
Solution 4 - Python
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__
intotest
- since
__enter__
doesn't have a return value,test
isNone
.
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.
Solution 5 - Python
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.