A "scopeguard" for Python

Robert Kern robert.kern at gmail.com
Thu Mar 4 00:28:35 CET 2010


On 2010-03-03 15:35 PM, Alf P. Steinbach wrote:
> * Robert Kern:
>> On 2010-03-03 13:32 PM, Alf P. Steinbach wrote:
>>> * Robert Kern:
>>>> On 2010-03-03 11:18 AM, Alf P. Steinbach wrote:
>>>>> * Robert Kern:
>>>>>> On 2010-03-03 09:56 AM, Alf P. Steinbach wrote:
>>>>>>> * Mike Kent:
>>>>>>>> What's the compelling use case for this vs. a simple try/finally?
>>>>>>>
>>>>>>> if you thought about it you would mean a simple "try/else".
>>>>>>> "finally" is
>>>>>>> always executed. which is incorrect for cleanup
>>>>>>
>>>>>> Eh? Failed execution doesn't require cleanup? The example you gave is
>>>>>> definitely equivalent to the try: finally: that Mike posted.
>>>>>
>>>>> Sorry, that's incorrect: it's not.
>>>>>
>>>>> With correct code (mine) cleanup for action A is only performed when
>>>>> action A succeeds.
>>>>>
>>>>> With incorrect code cleanup for action A is performed when A fails.
>>>>
>>>> Oh?
>>>>
>>>> $ cat cleanup.py
>>>>
>>>> class Cleanup:
>>>> def __init__( self ):
>>>> self._actions = []
>>>>
>>>> def call( self, action ):
>>>> assert( callable( action ) )
>>>> self._actions.append( action )
>>>>
>>>> def __enter__( self ):
>>>> return self
>>>>
>>>> def __exit__( self, x_type, x_value, x_traceback ):
>>>> while( len( self._actions ) != 0 ):
>>>> try:
>>>> self._actions.pop()()
>>>> except BaseException as x:
>>>> raise AssertionError( "Cleanup: exception during cleanup" )
>>>>
>>>> def print_(x):
>>>> print x
>>>>
>>>> with Cleanup() as at_cleanup:
>>>> at_cleanup.call(lambda: print_("Cleanup executed without an
>>>> exception."))
>>>>
>>>> with Cleanup() as at_cleanup:
>>>
>>> *Here* is where you should
>>>
>>> 1) Perform the action for which cleanup is needed.
>>>
>>> 2) Let it fail by raising an exception.
>>>
>>>
>>>> at_cleanup.call(lambda: print_("Cleanup execute with an exception."))
>>>> raise RuntimeError()
>>>
>>> With an exception raised here cleanup should of course be performed.
>>>
>>> And just in case you didn't notice: the above is not a test of the
>>> example I gave.
>>>
>>>
>>>> $ python cleanup.py
>>>> Cleanup executed without an exception.
>>>> Cleanup execute with an exception.
>>>> Traceback (most recent call last):
>>>> File "cleanup.py", line 28, in <module>
>>>> raise RuntimeError()
>>>> RuntimeError
>>>>
>>>>>> The actions are always executed in your example,
>>>>>
>>>>> Sorry, that's incorrect.
>>>>
>>>> Looks like it to me.
>>>
>>> I'm sorry, but you're
>>>
>>> 1) not testing my example which you're claiming that you're testing, and
>>
>> Then I would appreciate your writing a complete, runnable example that
>> demonstrates the feature you are claiming. Because it's apparently not
>> "ensur[ing] some desired cleanup at the end of a scope, even when the
>> scope is exited via an exception" that you talked about in your
>> original post.
>>
>> Your sketch of an example looks like mine:
>>
>> with Cleanup as at_cleanup:
>> # blah blah
>> chdir( somewhere )
>> at_cleanup.call( lambda: chdir( original_dir ) )
>> # blah blah
>>
>> The cleanup function gets registered immediately after the first
>> chdir() and before the second "blah blah". Even if an exception is
>> raised in the second "blah blah", then the cleanup function will still
>> run. This would be equivalent to a try: finally:
>>
>> # blah blah #1
>> chdir( somewhere )
>> try:
>> # blah blah #2
>> finally:
>> chdir( original_dir )
>
> Yes, this is equivalent code.
>
> The try-finally that you earlier claimed was equivalent, was not.

Okay, but just because of the position of the chdir(), right?

>> and not a try: else:
>>
>> # blah blah #1
>> chdir( somewhere )
>> try:
>> # blah blah #2
>> else:
>> chdir( original_dir )
>
> This example is however meaningless except as misdirection. There are
> infinitely many constructs that include try-finally and try-else, that
> the with-Cleanup code is not equivalent to. It's dumb to show one such.
>
> Exactly what are you trying to prove here?

I'm just showing you what I thought you meant when you told Mike that he should 
have used a try/else instead of try/finally.

> Your earlier claims are still incorrect.
>
>> Now, I assumed that the behavior with respect to exceptions occurring
>> in the first "blah blah" weren't what you were talking about because
>> until the chdir(), there is nothing to clean up.
>>
>> There is no way that the example you gave translates to a try: else:
>> as you claimed in your response to Mike Kent.
>
> Of course there is.
>
> Note that Mike wrapped the action A within the 'try':
>
>
> <code author="Mike" correct="False">
> original_dir = os.getcwd()
> try:
> os.chdir(somewhere)
> # Do other stuff
> finally:
> os.chdir(original_dir)
> # Do other cleanup
> </code>
>
>
> The 'finally' he used, shown above, yields incorrect behavior.
>
> Namely cleanup always, while 'else', in that code, can yield correct
> behavior /provided/ that it's coded correctly:
>
>
> <code author="Alf" correct="ProbablyTrue" disclaimer="off the cuff">
> original_dir = os.getcwd()
> try:
> os.chdir(somewhere)
> except Whatever:
> # whatever, e.g. logging
> raise
> else:
> try:
> # Do other stuff
> finally:
> os.chdir(original_dir)
> # Do other cleanup
> </code>

Ah, okay. Now we're getting somewhere. Now, please note that you did not have 
any except: handling in your original example. So Mike made a try: finally: 
example to attempt to match the semantics of your code. When you tell him that 
he should 'mean a simple "try/else". "finally" is always executed. which is 
incorrect for cleanup', can you understand why we might think that you were 
saying that try: finally: was wrong and that you were proposing that your code 
was equivalent to some try: except: else: suite?

>>> 2) not even showing anything about your earlier statements, which were
>>> just incorrect.
>>>
>>> You're instead showing that my code works as it should for the case that
>>> you're testing, which is a bit unnecessary since I knew that, but thanks
>>> anyway.
>>
>> It's the case you seem to be talking about in your original post.
>
> What's this "seems"? Are you unable to read that very short post?

I say "seems" because my understandings of what you meant in your original post 
and your response to Mike disagreed with one another. Now I see that your later 
posts were talking about minor discrepancy about which errors you wanted caught 
by the finally: and which you didn't. I was confused because it seemed that you 
were saying that try: finally: was completely wrong and that "try/else" was 
right. It confused me and at least one other person.

>> , but I do ask you to acknowledge that you originally were talking
>> about a feature that "ensure[s] some desired cleanup at the end of a
>> scope, even when the scope is exited via an exception."
>
> Yes, that's what it does.
>
> Which is I why I wrote that.
>
> This should not be hard to grok.
>
>
>> Do you acknowledge this?
>
> This seems like pure noise, to cover up that you were sputing a lot of
> incorrect statements earlier.

No, I'm just trying to clarify what you are trying to say. The above statement 
did not appear to accord with your later statement: 'if you thought about it you 
would mean a simple "try/else". "finally" is always executed. which is incorrect 
for cleanup.' It turns out that what you really meant was that it would be 
incorrect for cleanup to be executed when an error occurred in the chdir() itself.

Now, I happen to disagree with that. There are a couple of ways to do this kind 
of cleanup depending on the situation. Basically, you have several different 
code blocks:

# 1. Record original state.
# 2. Modify state.
# 3. Do stuff requiring the modified state.
# 4. Revert to the original state.

Depending on where errors are expected to occur, and how the state needs to get 
modified and restored, there are different ways of arranging these blocks. The 
one Mike showed:

# 1. Record original state.
try:
     # 2. Modify state.
     # 3. Do stuff requiring the modified state.
finally:
     # 4. Revert to the original state.

And the one you prefer:

# 1. Record original state.
# 2. Modify state.
try:
     # 3. Do stuff requiring the modified state.
finally:
     # 4. Revert to the original state.

These differ in what happens when an error occurs in block #2, the modification 
of the state. In Mike's, the cleanup code runs; in yours, it doesn't. For 
chdir(), it really doesn't matter. Reverting to the original state is harmless 
whether the original chdir() succeeds or fails, and chdir() is essentially 
atomic so if it raises an exception, the state did not change and nothing needs 
to be cleaned up.

However, not all block #2s are atomic. Some are going to fail partway through 
and need to be cleaned up even though they raised an exception. Fortunately, 
cleanup can frequently be written to not care whether the whole thing finished 
or not.

Both formulations can be correct (and both work perfectly fine with the chdir() 
example being used). Sometimes one is better than the other, and sometimes not. 
You can achieve both ways with either your Cleanup class or with try: finally:.

I am still of the opinion that Cleanup is not an improvement over try: finally: 
and has the significant ugliness of forcing cleanup code into callables. This 
significantly limits what you can do in your cleanup code.

-- 
Robert Kern

"I have come to believe that the whole world is an enigma, a harmless enigma
  that is made terrible by our own mad attempt to interpret it as though it had
  an underlying truth."
   -- Umberto Eco




More information about the Python-list mailing list