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__
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.
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.