Alternative cleanup handlers
You want to ensure the calling of
GPIO.cleanup, which seems to be recommended way of business for the Raspberry PI as it otherwise would leave the output pins in the current state, which could risk shortcuts later on if you reconnect a high pin to ground. See How to Exit GPIO programs cleanly, avoid warnings and protect your Pi.
This however is actually a general question on how to ensure calling of cleanup procedure when I'm done doing something. This has at least the following solutions:
Manual handling – You can always call
GPIO.cleanup() manually. Drawback of this is to remember doing it, and correctly do it when handling exceptions and other error situations
Trigger at exit – Using at_exit will ensure that it is always called, unless abnormal termination of your script. Does however need a little tweaking if you by design want to leave pins in the high state.
Using context managers – Allowing you to do
with ... as a wrapper, like in OP code, before ending by doing the needed cleanup. For more on this see With Statements Context Managers
Using decorators – The goal of cleaning up can also be met using decorators, also examplified by OP, where one can ensure the cleanup after the function finishes. See Decorators for Functions and methods (also see functools.wraps).
I also found a non-official article, "Python Decorators II: Decorator Arguments", related to decorators with or without arguements, both class versions and functions versions.
Through encapsulation and class destructors – When a class instantation is freed, there is a certain possibilty that the
__del__ function is called. There are some caveats related to destructores, see "Safely using destructors in Python", related to circular reference counting and exception in constructors.
Why choose a specific handler
Your specific use cases will affect your choice of handler for cleaning up, and there can be large variations on needs and requirements. Here are some arguments related to the different options.
In the general case, using manual handling is unreliable as we're humans and we tend to forget to do the right thing. Having the option available though, is useful. The largest caveat for this is when exceptions and errors occur to then properly assure the cleanup actions to be taken.
Trigger at exit
Using a trigger at exit is, in my opinion, the safest option, as you then have made a conscious decision to always cleanup when script exits. This will catch all cases, except abnormal termination of the script, i.e. through external signals or similar. Exceptions doesn't not prevent the exit methods to be called.
However this option needs to have an option to actually allow cleanup not to take place if your script is use to handle a smart home where the pins needs to stay in their low or high position after script termination as you might want to use your Raspberry Pi to do other stuff in between. This does require proper handling of warnings when reconnecting on later invocations.
Context manager does have their purpose and right of life, and it possibly comes down to personal choice whether to use these or not. They do have the element of remembering to use them, but when remembered they do the job.
One minor detail is that they increase indentation levels, and it can be hard to find the proper level of when to do it as the scope of this context might be rather large, spanning several functions.
And in my opinion I would like for a context manager to be in as narrow a scope as possible, without too many large functions called from it. That is to have at little code as possible within the scope of a context manager. Related to file handling, I would like for the file handler to be open for a shortest possible amount of time.
My opinion on this is most likely based on the concept of not clogging up resources, and releasing that at the earliest point of convenience. This might not apply though, as the requirement to cleaning up for GPIO is not a matter of resource allocation.
Decorators seems like a more natural choice to me, as they decorate a function and you can then give a proper bounding for when you need the cleanup to occur.
The concept of resource allocations doesn't apply as a decorator is by convention something to do in front or after functions, and I think this use of a decorator is a good choice.
Cleaning up through encapsulation
My last option is to use cleanup through encapsulation. That is to create a class which fetches or references the
GPIO, and then when the object is freed it calls the cleanup action. At first this might seem like an extra step considered to other methods, but one advantage is that this could allow for proper encapsulation of more than just the cleanup.
It's not unlikely that you have a dedicated script to control a given set of pins, which lends it self to being encapsulated as a class where you hide the actual operation on the pins themselves, and instead provide helper methods like
engage_left_engine(state), ... That is you give your script higher level methods doing the logic, and not implicitly needing to do the
GPIO.output(pin, value) with magic numbers.
This would allow for easier refactoring of pin layout, and a lot easier code to read at higher levels. And if doing this, it would also make sense to have the cleanup being a part of the class so that when freeing the class, the pins are cleaned up as well.
To be on the safe side I would implement the trigger at exit with an optional module method to disable the cleanup in the rare cases where you want to leave the pins in their current state. This would allow for an almost guaranteed cleanup unless you specific says to do otherwise.
Secondly I would use decorators given a low level script for intermediate cleanup of pins throughout various parts of script. Possibly opt'ing for the use of encapsulation for a larger system like if you build a smart home system, where it would make sense to encapsulate the different pins and have better names to address the different switches.