Optionally add Python attributes to DeviceProxy instances

Prior to PyTango 9.3.4, developers could add arbitrary Python attributes to a DeviceProxy instance. From version 9.3.4 PyTango raises an exception if this is attempted. To aid backwards compatibility, where the old use case was beneficial, some new methods have been added to the DeviceProxy. We use the term dynamic interface to refer this. When the dynamic interface is frozen (the default) it cannot be changed, and you get an exception if you try. Unfreeze it by calling unfreeze_dynamic_interface() if you want to make these kinds of changes. Here is an example:

>>> import tango
>>> dp = tango.DeviceProxy("sys/tg_test/1")
>>> dp.is_dynamic_interface_frozen()
True
>>> dp.non_tango_attr = 123
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/lib/python3.11/site-packages/tango/device_proxy.py", line 484, in __DeviceProxy__setattr
    raise e from cause
  File "/lib/python3.11/site-packages/tango/device_proxy.py", line 478, in __DeviceProxy__setattr
    raise AttributeError(
AttributeError: Tried to set non-existent attr 'non_tango_attr' to 123.
The DeviceProxy object interface is frozen and cannot be modified - see tango.DeviceProxy.freeze_dynamic_interface for details.
>>> dp.unfreeze_dynamic_interface()
/lib/python3.11/site-packages/tango/device_proxy.py:302: UserWarning: Dynamic interface unfrozen on DeviceProxy instance TangoTest(sys/tg_test/1) id=0x102a4ea20 - arbitrary Python attributes can be set without raising an exception.
  warnings.warn(
>>> dp.non_tango_attr = 123
>>> dp.non_tango_attr
123
>>> dp.freeze_dynamic_interface()
>>> dp.is_dynamic_interface_frozen()
True