Open jeanchristopheruel opened 2 years ago
The context manager is a great tool provided by python to provide a runtime context to some variables. It makes it possible to ensure that, even in the case of errors inside the context, some sort of cleanup takes place. In that regard it can be considered similar to the CPP concept or RAII. It is used with some of the core classes in VimbaPython to perform this exact task, ensure that the Vimba
context is entered and the VmbStartup
/VmbShutdown
functions of our C-API are called properly, and to make sure the camera connection is handled appropriately across the lifetime of the Camera
instance.
From my personal experience it is usually possible to design software in such a way, that the context manager can be used to handle these tasks nicely. For multithreaded operations for example entering the Camera
s context manager in a core thread that runs some kind of "producer" class might be a good approach (see e.g. the multithreading_opencv.py
example). But I do understand that this might not fit all use cases.
Pythons implementation of the context manager can be considered syntactic sugar for something like the following, where instance
is our variable for which we want to enter the context manager.
try:
instance.enter_context_manager()
instance.do_something()
finally:
instance.leave_context_manager()
This will basically do the same thing I described above:
enter_context_manager
) to prepare it for usedo_something
)finally ... leave_context_manager
)For the actual implementation of Pythons context managers, these enter_context_manager
and leave_context_manager
methods use the "magic" names __enter__
and __exit__
. See also PEP-343.
With that in mind, the current implementation of VimbaPython actually already makes it possible to do this manual managing of the context. But it requires the user to call these "magic" methods manually. Here is an example how it could be used. Note that managing the context manually is not recommended or intended by us and could lead to hard to debug issues.
class Camera:
def __init__(self) -> None:
self.context_entered = False
def __enter__(self):
print('Entering the context manager')
self.context_entered = True
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
print('Leaving the context manager')
self.context_entered = False
def do_something(self):
if self.context_entered:
print('Successfully did something')
else:
print('Failed to do something. Context was not entered')
print('A'*50)
print('Using the context manager normally')
with Camera() as cam:
cam.do_something()
cam.do_something()
print('\n' + 'B'*50)
print('Using the context manager but getting an exception')
try:
with Camera() as cam:
cam.do_something()
raise Exception(f'Something went wrong!')
cam.do_something()
except Exception as e:
# Ignore the exception so the other examples still run
print(f'Exception was caught: {e}')
print('\n' + 'C'*50)
print('Manually managing context')
cam = Camera()
cam.__enter__()
cam.do_something()
cam.do_something()
cam.__exit__(None, None, None)
print('\n' + 'D'*50)
print('Manually managing context but getting an exception')
cam = Camera()
try:
cam.__enter__()
cam.do_something()
raise Exception(f'Something went wrong!')
cam.do_something()
cam.__exit__(None, None, None)
except Exception as e:
# Ignore the exception so the other examples still run
print(f'Exception was caught: {e}')
print(f'WARNING: Context of cam was never closed! cam.context_entered={cam.context_entered}')
print('\n' + 'E'*50)
print('Manually managing context but getting an exception with proper handling of __exit__')
cam = Camera()
try:
cam.__enter__()
cam.do_something()
raise Exception(f'Something went wrong!')
cam.do_something()
except Exception as e:
# Ignore the exception so the other examples still run
print(f'Exception was caught: {e}')
finally:
cam.__exit__(None, None, None)
print(f'This time context of cam was closed properly thanks to the `finally` block. cam.context_entered={cam.context_entered}')
The output should show the following to demonstrate the intricacies of the different uses
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Using the context manager normally
Entering the context manager
Successfully did something
Successfully did something
Leaving the context manager
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
Using the context manager but getting an exception
Entering the context manager
Successfully did something
Leaving the context manager
Exception was caught: Something went wrong!
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
Manually managing context
Entering the context manager
Successfully did something
Successfully did something
Leaving the context manager
DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
Manually managing context but getting an exception
Entering the context manager
Successfully did something
Exception was caught: Something went wrong!
WARNING: Context of cam was never closed! cam.context_entered=True
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
Manually managing context but getting an exception with proper handling of __exit__
Entering the context manager
Successfully did something
Exception was caught: Something went wrong!
Leaving the context manager
This time context of cam was closed properly thanks to the `finally` block. cam.context_entered=False
As you can see the cases B
and E
are almost the same. The only difference is in the order in which the caught exception are handled and the context manager are closed. This is just because the except
part of the try ... except ... finally
block is executed before the finally
part. For practical use I do not think that this should have much of an impact.
__enter__
and __exit__
methods need to be called in the appropriate order with
statement is the intended and recommended approach, as this greatly simplifies the proper closing of resources.PS: Thank you for the input on this. You seem to be quite knowledgeable in these more detailed design choices. Do you have a suggestion how we could better allow for both paradigms to be used, with
context manager and manual management?
Thank you so much so your fast and valuable reply !
In multithread_opencv at line 131, __call__
has a direct dependency to the camera context manager, which breaks the encapsulation principle.
The with statement prohibit the user from defining granular exception catching based on the type. See Catching an exception while using a Python 'with' statement
In order to deal with encapsulation and granular exception catching, I would personally prefer something like:
import vimba
import threading
import queue
import typing
class Camera(threading.Thread):
"""Frame producer thread"""
_frame_queue: queue.Queue
_vimda_cam: vimba.Camera
def __init__(self, vimba_instance: vimba.vimba.Vimba._Vimba__Impl, ...) -> None:
# Do something like assert vimba_instance.is_running()
super().__init__()
self._frame_queue = queue.Queue()
try:
self._vimba_cam = vimba_instance.get_camera_by_id(...)
except ...:
...
finally:
...
...
@utils.override
def set_up(self) -> None:
"""Called just before main thread enters"""
self._vimba_cam._open()
_configure_camera(self._vimba_cam, ...)
@utils.override
def teardown(self) -> None:
"""Called just after main thread exits"""
try:
self._vimba_cam._close()
except ...:
...
finally:
...
@utils.override
def run(self) -> None:
def frame_handler(cam: vimba.Camera, frame: vimba.Frame) -> None:
if frame.get_status() == vimba.FrameStatus.Complete:
self._last_frame.set_value(frame.as_numpy_ndarray())
cam.queue_frame(frame)
self._vimba_cam.start_streaming(handler=frame_handler, buffer_count=10)
while ...:
time.sleep(0.001)
pass
self._vimba_cam.stop_streaming()
self.reset_frame()
def main() -> None:
try:
vimba_instance: vimba.vimba.Vimba._Vimba__Impl = vimba.Vimba.get_instance()
vimba_instance._startup()
except ...:
...
finally:
...
cameras: typing.List[Camera] = []
for ... in ...:
cameras.append( Camera(vimba_instance, ...) )
for cam in cameras:
try:
cam.start()
except ...:
...
finally:
...
while ...:
# Hardware triggering
gpio_trigger.on()
for cam in cameras:
cam.wait_for_new_frame(timeout=2.0)
gpio_trigger.off()
for cam in cameras:
if cam.is_frame_available():
frame = cam.get_frame()
...
for cam in cameras:
try:
cam.stop(blocking=True, timeout=1.0)
except ...:
...
finally:
...
try:
vimba_instance._shutdown()
except ...:
...
finally:
...
The thing is that if something goes wrong with the vimba resource deallocation, I could not find a way of recovering appart from restarting the usb controller. (Related to #117 ). I would suggest to make some kind of daemon driver that can handle multiple processes instead of the vimba instance singleton.
Regarding the __enter__
and __exit__
methods, since they are both special functions, it is not conventional to let the user explicitly call them. One would expect the class to have a public interface. Same thing would apply for _open
and _close
as the prefix underscore is usually meant to declare a private function.
Finally, I understand that my needs may not represent those of an average customer. I am trying to make a multi-sensor IoT camera with fully containerized software. I really enjoy working with your cameras and I think you had an excellent idea by hosting you API as an open source repo on github ! Keep up the good work !
Allow the user to avoid using the "with" statement to manually manage contexts.
As mentioned by some users, it seems like the "with" statement is not the best choice of design for applications where the context manager need to be accessed across multiple threads. See #47 and #48.
I suggest to refactor the vimba Instance (vimba.vimba.Vimba._Vimba__Impl) and Camera (vimba.Camera) to allow the user to manually manage contexts. I would also improve vimba.vimba.Vimba's typing formats (ex: vimba.vimba.Vimba._Vimba__Impl -> vimba.Instance).
For example: