When writing PyTango device servers, it is common to implement one Python class per Tango device class. For small projects, this approach is simple and easy to understand. However, it becomes cumbersome when the set of available devices is not known at development time.
Consider a device server that should be entirely driven by a configuration file. Instead of hard-coding every supported device class, the server reads the device definitions at startup and creates the required Tango device classes automatically.
The goal is to write a generic PyTango server that does not need to be modified whenever a new device class is added. If a new entry appears in the configuration file, the server should simply create the corresponding Tango device class.
Python type
At first glance, this sounds unusual. After all, Python classes are typically defined using the familiar class keyword:
class Motor(Device):
pass
Most Python developers stop here and never think about how classes are actually created. Under the hood, however, classes are objects themselves, and Python provides a built-in mechanism to construct them dynamically.
The function responsible for this is type().
Most of us use it in its simplest form to inspect the type of an object:
print(type(42))
# <class 'int'>
Less well known is its three-argument form:
type(name, bases, attributes)
The arguments are:
name: the name of the new class.bases: a tuple of parent classes.attributes: a dictionary containing class attributes and methods.
This means that the following definition is equivalent to the above class definition.
Motor = type(
"Motor",
(Device,),
{},
)
The resulting object is exactly the same: a Python class that can be instantiated or registered with PyTango.
The third argument of type() becomes particularly interesting when more than just the class name should be configurable. The attributes dictionary allows methods, properties, or other class members to be added dynamically. This can be useful when Tango attributes or commands are also described in the configuration file.
Motor = type(
"Motor",
(Device,),
{
"some_property": 42,
},
)
a = Motor()
print(a.some_property)
# 42
Conclusion
For our use case, creating classes dynamically becomes straightforward. Reading the configuration file and creating the required classes can be done in a simple loop. From PyTango’s perspective, there is no difference between a statically defined class and one created dynamically using type(). Both behave like ordinary Python classes.
