Are there likely to be some negative consequences of this decision?
Absolutely.
- Calling
help(DocumentObject) will not tell you what Property attributes exist in your class.
- An IDE won't have any information for autocomplete. Eg) Typing
brick. and pressing the <TAB> key won't offer length and width as possible completions.
- Callers can add, remove and change elements of
brick.properties.
We can get around all of this by defining your own data descriptors.
Reworked Code
Data descriptor
First, let's create a data descriptor: a class with a __get__ and __set__ methods. This will allow us to defined a Property on the the DocumentObject class, and control the way things are read from or written to instances of DocumentObject class instances through those properties.
The name, default value and units of the property can be stored in the property descriptor, since they are read-only values.
We'll also create a Property.Instance class to hold the data in an instance of the the property in the DocumentObject. Instances of the Property.Instance will have the read-write attributes of value, and visible, as well as a link to the property descriptor for the read-only values.
__slots__ is used to prevent additional fields from being set on a property.
class Property:
__slots__ = ('name', 'default', 'units', '__doc__',)
def __init__(self, default, units, doc):
self.default = default
self.units = units
self.__doc__ = doc
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner=None):
if instance is None:
return self
prop = instance._get_property(self.name)
return prop.value
def __set__(self, instance, value):
prop = instance._get_property(self.name)
prop.value = value
class Instance:
__slots__ = ('value', '_visible', '_property')
def __init__(self, prop):
self._property = prop
self.value = prop.default
self._visible = True
@property
def name(self):
return self._property.name
@property
def units(self):
return self._property.units
@property
def visible(self):
return self._visible
@visible.setter
def visible(self, value):
self._visible = bool(value)
def __repr__(self):
return f"Prop[{self.name} {self.value} {self.units} {self.visible}]"
Properties
For each DocumentObject, we'll want a container for all of the properties. The _get_property() method we used, above, exacts a named property instance from the container.
Since we want this container to be a fixed size, with only the named properties defined on the class, we'll create a named tuple with those property instances.
To make life easy, we'll create the namedtuple of property instances automatically, when the subclass is defined.
from collections import namedtuple
class Property:
...
class Properties:
def __init_subclass__(cls):
cls._property_list = [attr for attr in vars(cls).values()
if isinstance(attr, Property)]
names = [prop.name for prop in cls._property_list]
properties_type = namedtuple(cls.__name__ + "Properties", names)
cls._properties_type = properties_type
def __init__(self):
property_list = self.__class__._property_list
properties_type = self.__class__._properties_type
properties = [Property.Instance(prop) for prop in property_list]
self._properties = properties_type._make(properties)
def _get_property(self, name):
return getattr(self._properties, name)
@property
def properties(self):
return self._properties
Creating the DocumentObject
Deriving the DocumentObject from the Properties class will automatically call the __init_subclass__ of the parent class. At this point, it collects all of the Property descriptors, and constructs the namedtuple type for the property container. During the actual super().__init__(), all of the property instances get created, and stored in an instance of the namedtuple type created for this purpose.
...
class DocumentObject(Properties):
def __init__(self):
super().__init__()
length = Property(10, "mm", "length of brick, in mm")
width = Property(5, "mm", "width of brick, in mm")
brick = DocumentObject()
print(brick.properties.length.name)
print(brick.length)
Seatbelts
Autocompletion
Typing brick. and pressing TAB can autocomplete with length or width (or properties), because those are now named attributes of the DocumentObject class.
Typing brick.properties. and pressing TAB will also suggest length and width as autocompletions for the property container.
Immutability
The caller cannot add or change brick.properties because it is an immutable named tuple.
Of course, the property instances are not immutable, so the the following are all allowed:
brick.properties.length.visible = False
brick.properties.length.value = 20
brick.length = 30
Help
Typing help(DocumentObject) now produces:
Help on class DocumentObject in module __main__:
class DocumentObject(Properties)
| ...
|
| Data descriptors defined here:
|
| length
| length of brick, in mm
|
| width
| width of brick, in mm
|
| ...
"length"; is the length attributes unit a fixed unit (such asmm), or can it be changed by the user (ie,brick.property["length"].units = "feet")? \$\endgroup\$unitwill probably be constant. But another attribute of Property would beisVisiblewhich would be a boolean settable by the user. I would implement this using a @property descriptors for isVisible. \$\endgroup\$