Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does Python have an equivalent to Haskell's 'mask' or 'bracket' functions?

In Haskell we have asynchronous exceptions; we can use throwTo to raise any exception in another thread:

throwTo :: Exception e => ThreadId -> e -> IO ()

throwTo raises an arbitrary exception in the target thread (GHC only).

To be able to write code with guarantees like "will always release a lock after acquiring it", we have mask to run code in which asynchronous exceptions may only be received while the computation is blocking:

mask :: ((forall a. IO a -> IO a) -> IO b) -> IO b

Executes an IO computation with asynchronous exceptions masked. That is, any thread which attempts to raise an exception in the current thread with throwTo will be blocked until asynchronous exceptions are unmasked again.

and a stronger uninterruptibleMask in which async exceptions will not be raised at all during a masked computation:

uninterruptibleMask :: ((forall a. IO a -> IO a) -> IO b) -> IO b

Like mask, but the masked computation is not interruptible

Masking is used to implement higher-level abstractions like bracket:

bracket
    :: IO a         -- computation to run first ("acquire resource")
    -> (a -> IO b)  -- computation to run last ("release resource")
    -> (a -> IO c)  -- computation to run in-between
    -> IO c         -- returns the value from the in-between computation

When you want to acquire a resource, do some work with it, and then release the resource, it is a good idea to use bracket, because bracket will install the necessary exception handler to release the resource in the event that an exception is raised during the computation. If an exception is raised, then bracket will re-raise the exception (after performing the release).

If I understand correctly, Python has a (less general) form of asynchronous exceptions, with the most notable manifestation being KeyboardInterrupt:

Raised when the user hits the interrupt key (normally Control-C or Delete). During execution, a check for interrupts is made regularly.

The documentation is imprecise about when the "check for interrupts" may occur, but it seems to imply that a KeyboardInterrupt may be raised at any point in a program's execution. So it seems, then, that Python's async exceptions come with all the same subtle difficulties of maintaining correctness.

For example, consider a pattern like this:

x = None
try:
    x = acquire()
    do_something(x)    # (1)
finally:
    if x is not None:  # (2)
        release(x)

If any exception is raised during (1), then we are assured that the contents of the finally block will be executed. But what happens if a KeyboardInterrupt is during (2)?

It seems fundamentally impossible to guarantee resource cleanup in the presence of asyc exceptions without a way to mask them. Is there some facility for this, or do we rely on the ostrich algorithm?

like image 495
Chris Martin Avatar asked Feb 10 '17 20:02

Chris Martin


1 Answers

This is what context managers are for.

with acquire() as x:
    do_something(x)

acquire returns an object whose type defines an __enter__ method, which returns the value bound to x, and an __exit__ method, which is executed at the end of the with statement regardless of how the statement is exited.

like image 100
chepner Avatar answered Nov 07 '22 18:11

chepner