I'm writing an MPI-based application (but MPI doesn't matter in my question, I mention it only to expose the rationale) and in some cases, when there is less work items than processes, I need to create a new communicator excluding the processes that have nothing to do. Finally, the new communicator has to be freed by the processes that have work to do (and only by them).
A neat way to do that would be to write:
with filter_comm(comm, nworkitems) as newcomm:
... do work with communicator newcomm...
the body being executed only by the processes that have work to do.
Is there a way in a context manager to avoid executing the body? I understand that context managers have rightfully been designed to avoid hiding control flows, but I wonder if it is possible to circumvent that, since in my case I think it would be justified for clarity sake.
The ability to conditionally skip context manager body has been proposed but rejected as documented in PEP 377.
I did some research about alternatives. Here are my findings.
First let me explain the background of my code examples. You have a bunch of devices you want to work with. For every device you have to acquire the driver for the device; then work with the device using the driver; and lastly release the driver so others can acquire the driver and work with the device.
Nothing out of the ordinary here. The code looks roughly like this:
driver = getdriver(devicename)
try:
dowork(driver)
finally:
releasedriver(driver)
But once every full moon when the planets are not aligned correctly the acquired driver for a device is bad and no work can be done with the device. This is no big deal. Just skip the device this round and try again next round. Usually the driver is good then. But even a bad driver needs to be released otherwise no new driver can be acquired.
(the firmware is proprietary and the vendor is reluctant to fix or even acknowledge this bug)
The code now looks like this:
driver = getdriver(devicename)
try:
if isgooddriver(driver):
dowork(driver)
else:
handledrivererror(geterrordetails(driver))
finally:
release(driver)
That is a lot of boilerplate code that needs to be repeated everytime work needs to be done with a device. A prime candidate for python's context manager also known as with statement. It might look like this:
# note: this code example does not work
@contextlib.contextmanager
def contextgetdriver(devicename):
driver = getdriver(devicename)
try:
if isgooddriver(driver):
yield driver
else:
handledrivererror(geterrordetails(driver))
finally:
release(driver)
And then the code when working with a device is short and sweet:
# note: this code example does not work
with contextgetdriver(devicename) as driver:
dowork(driver)
But this does not work. Because a context manager has to yield. It may not not yield. Not yielding will result in a RuntimeException
raised by contextmanager
.
So we have to pull out the check from the context manager
@contextlib.contextmanager
def contextgetdriver(devicename):
driver = getdriver(devicename)
try:
yield driver
finally:
release(driver)
and put it in the body of the with
statement
with contextgetdriver(devicename) as driver:
if isgooddriver(driver):
dowork(driver)
else:
handledrivererror(geterrordetails(driver))
This is ugly because now we again have some boilerplate that needs to be repeated everytime we want to work with a device.
So we want a context manager that can conditionaly execute the body. But we have none because PEP 377 (suggesting exactly this feature) was rejected.
Instead of not yielding we can raise an Exception ourselves:
@contextlib.contextmanager
def contextexceptgetdriver(devicename):
driver = getdriver(devicename)
try:
if isgooddriver(driver):
yield driver
else:
raise NoGoodDriverException(geterrordetails(driver))
finally:
release(driver)
but now you need to handle the exception:
try:
with contextexceptgetdriver(devicename) as driver:
dowork(driver)
except NoGoodDriverException as e:
handledrivererror(e.errordetails)
which has almost the same cost of code complexity as the explicit testing for good driver above.
The difference: with an exception we can decide to not handle it here and instead let it bubble up the call stack and handle it elsewhere.
Also difference: by the time we handle the exception the driver has already been released. While with the explicit check the driver has not been released. (the except is outside of the with statement while the else is inside the with statement)
I found that abusing a generator works quite well as a replacement of a context manager which can skip the body
def generatorgetdriver(devicename):
driver = getdriver(devicename)
try:
if isgooddriver(driver):
yield driver
else:
handledrivererror(geterrordetails(driver))
finally:
release(driver)
But then the calling code looks very much like a loop
for driver in generatorgetdriver(devicename):
dowork(driver)
If you can live with this (please don't) then you have a context manager that can conditionaly execute the body.
It seems hat the only way to prevent the boilerplate code is with a callback
def workwithdevice(devicename, callback):
driver = getdriver(devicename)
try:
if isgooddriver(driver):
callback(driver)
else:
handledrivererror(geterrordetails(driver))
finally:
release(driver)
And the calling code
workwithdevice(devicename, dowork)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With