Python package unloaded before __del__ is called

495 Views Asked by At

I am using pyvisa to communicate via USB with an instrument. I am able to control it properly. Since it is a high voltage source, and it is dangerous to forget it with high voltage turned on, I wanted to implement the __del__ method in order to turn off the output when the code execution finishes. So basically I wrote this:

import pyvisa as visa

class Instrument:
    def __init__(self, resource_str='USB0::1510::9328::04481179::0::INSTR'):
        self._resource_str = resource_str
        self._resource = visa.ResourceManager().open_resource(resource_str)
    
    def set_voltage(self, volts: float):
        self._resource.write(f':SOURCE:VOLT:LEV {volts}')
    
    def __del__(self):
        self.set_voltage(0)

instrument = Instrument()
instrument.set_voltage(555)

The problem is that it is not working and in the terminal I get

$ python3 comunication\ test.py 
Exception ignored in: <function Instrument.__del__ at 0x7f4cca419820>
Traceback (most recent call last):
  File "comunication test.py", line 12, in __del__
  File "comunication test.py", line 9, in set_voltage
  File "/home/superman/.local/lib/python3.8/site-packages/pyvisa/resources/messagebased.py", line 197, in write
  File "/home/superman/.local/lib/python3.8/site-packages/pyvisa/resources/messagebased.py", line 157, in write_raw
  File "/home/superman/.local/lib/python3.8/site-packages/pyvisa/resources/resource.py", line 190, in session
pyvisa.errors.InvalidSession: Invalid session handle. The resource might be closed.

I guess that what is happening is that pyvisa is being "deleted" before the __del__ method of my object is being called. How can I prevent this? How can I tell Python that pyvisa is "important" for objects of the Instrument class so it is not unloaded until all of them have been destroyed?

4

There are 4 best solutions below

0
On BEST ANSWER

Finally I found my answer here using the package atexit. This does exactly what I wanted to do (based on my tests up to now):

import pyvisa as visa
import atexit

class Instrument:
    def __init__(self, resource_str):
        self._resource = visa.ResourceManager().open_resource(resource_str)
        
        # Configure a safe shut down for when the class instance is destroyed:
        def _atexit():
            self.set_voltage(0)
        atexit.register(_atexit) # https://stackoverflow.com/a/41627098
    
    def set_voltage(self, volts: float):
        self._resource.write(f':SOURCE:VOLT:LEV {volts}')
    
instrument = Instrument(resource_str = 'USB0::1510::9328::04481179::0::INSTR')
instrument.set_voltage(555)

The advantage of this solution is that it is user-independent, it does not matter how the user instantiates the Instrument class, in the end the high voltage will be turned off.

0
On

In general, you cannot assume that __del__ will be called. If you're coming from an RAII (resource allocation is initialization) language such as C++, Python makes no similar guarantee of destructors.

To ensure some action is reversed, you should consider an alternative such as context managers:

from contextlib import contextmanager

@contextmanager
def instrument(resource_str='USB0::1510::9328::04481179::0::INSTR'):
    ... 
    try:
        ... # yield something
    finally:
        # set voltage of resource to 0 here

You would use it like

with instrument(<something>) as inst:
   ... 
# guaranteed by here to set to 0.
0
On

I believe Ami Tavory's answer is generally considered to be the recommended solution, though context managers aren't always suitable depending on how the application is structured.

The other option would be to explicitly call the cleanup functions when the application is exiting. You can make it safer by wrapping the whole application in a try/finally, with the finally clause doing the cleanup. Note that if you don't include a catch then the exception will be automatically re-raised after executing the finally, which may be what you want. Example:

app = Application()
try:
    app.run()
finally:
    app.cleanup()

Be aware, though, that you potentially just threw an exception. If the exception happened, for example, mid-communication then you may not be able to send the command to reset the output as the device could be expecting you to finish what you had already started.

2
On

I faced the same kind of safety issue with another type of connected device. I could not predict safely the behavior of the __del__ method as discussed in questions like I don't understand this python __del__ behaviour. I ended with a context manager instead. It would look like this in your case:

    def __enter__(self):
        """
        Nothing to do.
        """
        return self

    def __exit__(self, type, value, traceback):
        """
        Set back to zero voltage.
        """
        self.set_voltage(0)

with Instrument() as instrument:
    instrument.set_voltage(555)