I am currently designing a configuration screen for my application. There are about 50 configuration parameters, which I have described in a CSV file, and read in Python.
Currently the fields for each parameter row are:
csv_row_count
cat_name
attr_name
disp_str
unit
data_type
min_val
max_val
def_val
incr
cur_val
desc
So a few examples for a parameter could be:
Every single parameter is then drawn in my application as UI for the user to interact with, and looks like this:
The csv_row_count
is later used as an idx
for looping. If I ever want to add another parameter to be configured, I would have to use a unique one and add the parameter to the bottom of the CSV.
I currently have a class which stores all the information into attributes and can be stored back to the CSV after changes by the user.
Now, the problem I am having is that I need to react to the changes the user makes in the configuration screen (uniquely for every single parameter).
So my idea was to create an ConfigEventHandler
class, that has an Event
for when the parameter is created (on boot of the application, to draw it to the UI for example) and an Event
which is fired when the value has been changed (to make changes to the value in the UI, and also react correspondingly for this specific setting that has been changed).
That class looks like this:
from axel import Event
import logging as log
class ConfigEventHandler:
def __init__(self):
# Make an Event for when a config row is created (usually only at boot of application)
self.on_create_ev = Event()
self.on_create_ev += self._on_create_event
# Make an Event for when a config row has changed. Fire-ing of this event will be after a change in the attribute.
self.on_change_ev = Event()
self.on_change_ev += self._on_value_change_event
def _on_create_event(self, idx):
pass
# log.info("Event OnCreate fired for IDX " + str(idx))
def _on_value_change_event(self, idx, user_dict):
log.info("Event On Value Change fired for IDX " + str(idx))
log.info("With user dict: " + str(user_dict))
And I fire these events on creation of the class and when the values change. The only way for this events to identify which value has been changed, is by the idx
value which correlates to the csv_row_count
attribute as described in the CSV.
This would mean that if I want to react to each parameter being changed, I would need to fill the _on_value_change_event
method with a lot of if
-statements, e.g.:
if idx == 0:
react_to_param0()
elif idx == 1:
react_to_param1()
etc...
I feel like this is really bad practice (in any program language), and would easily break if someone screwed around with the idx
numbering in the CSV file (E.G. me in a few months, or the person that will take over my project).
So my question is, what would be a good alternative for fixing this relationship issue in a way that is more clean than a lot of if
-statements and a relationship between a list idx
and CSV index to make it more future proof, and keep the code readable?
I have thought of making a seperate class instance for each congifurable parameter, but don't really see how that would break the 'fragile' seeming relation between the csv_row_count
/idx
and the unique functions that react to changes to one specific parameter.
Doing things in a "pythonic" way often involves using dictionaries, one of the language's primary data-structures which have been highly optimized for speed and minimal memory usage.
With that in mind, the first thing I'd do is to get the configuration parameters in the CSV file into one. Since there's one per row of the CSV file, a logical thing to do would be to use a csv.DictReader
to read them since it returns each row as a dictionary of values. You can easily build a dictionary-of-dictionaries out of those by using the unique csv_row_count
(aka idx
) field of each one as the key for upper-level dict
.
Here's what I mean:
import csv
from pprint import pprint
CONFIG_FILEPATH = 'parameters.csv'
config_params = {}
with open(CONFIG_FILEPATH, 'r', newline='') as configfile:
reader = csv.DictReader(configfile, escapechar='\\')
csv_row_count = reader.fieldnames[0] # First field is identifier.
for row in reader:
idx = int(row.pop(csv_row_count)) # Remove and convert to integer.
config_params[idx] = row
print('config_params =')
pprint(config_params, sort_dicts=False)
So if your CSV file contained the rows like this:
csv_row_count,cat_name,attr_name,disp_str,unit,data_type,min_val,max_val,def_val,incr,cur_val,desc
15,LEDS,led_gen_use_als,Automatic LED Brightness,-,bool,0.0,1.0,TRUE,1.0,TRUE,Do you want activate automatic brightness control for all the LED's?
16,LEDS,led_gen_als_led_modifier,LED Brightness Modifier,-,float,0.1,1.0,1,0.05,1,The modifier for LED brightness. Used as static LED brightness value when 'led_gen_use_als' == false.
17,LEDS,led_gen_launch_show,Enable launch control LEDs,-,bool,0.0,1.0,TRUE,1.0,TRUE,Show when launch control has been activated\, leds will blink when at launch RPM
Reading it with the above code would result in the following dictionary-of-dictionaries being built:
config_params =
{15: {'cat_name': 'LEDS',
'attr_name': 'led_gen_use_als',
'disp_str': 'Automatic LED Brightness',
'unit': '-',
'data_type': 'bool',
'min_val': '0.0',
'max_val': '1.0',
'def_val': 'TRUE',
'incr': '1.0',
'cur_val': 'TRUE',
'desc': 'Do you want activate automatic brightness control for all the '
"LED's?"},
16: {'cat_name': 'LEDS',
'attr_name': 'led_gen_als_led_modifier',
'disp_str': 'LED Brightness Modifier',
'unit': '-',
'data_type': 'float',
'min_val': '0.1',
'max_val': '1.0',
'def_val': '1',
'incr': '0.05',
'cur_val': '1',
'desc': 'The modifier for LED brightness. Used as static LED brightness '
"value when 'led_gen_use_als' == false."},
17: {'cat_name': 'LEDS',
'attr_name': 'led_gen_launch_show',
'disp_str': 'Enable launch control LEDs',
'unit': '-',
'data_type': 'bool',
'min_val': '0.0',
'max_val': '1.0',
'def_val': 'TRUE',
'incr': '1.0',
'cur_val': 'TRUE',
'desc': 'Show when launch control has been activated, leds will blink '
'when at launch RPM'}}
Now, as far as reacting to each parameter change goes, one way to do it would be by using a function decorator to define when the function of the decorator it is applied to will get called based on the value of its first argument. This will eliminate the need for a long series of if
-statements in your _on_value_change_event()
method — because it can be replaced with a single call to the named decorated function.
I got the idea for implementing a decorator to achieve this from the article Learn About Python Decorators by Writing a Function Dispatcher, although I have implemented it in a slightly different manner using a class (instead of a function with nested-functions), which I think is a little "cleaner". Also note how the decorator itself uses an internal dictionary named registry
to do the store information about and to do the actual dispatching — another "pythonic" way to do things.
Again, here's what I mean:
class dispatch_on_value:
""" Value-dispatch function decorator.
Transforms a function into a value-dispatch function, which can have
different behaviors based on the value of its first argument.
See: http://hackwrite.com/posts/learn-about-python-decorators-by-writing-a-function-dispatcher
"""
def __init__(self, func):
self.func = func
self.registry = {}
def dispatch(self, value):
try:
return self.registry[value]
except KeyError:
return self.func
def register(self, value, func=None):
if func is None:
return lambda f: self.register(value, f)
self.registry[value] = func
return func
def __call__(self, *args, **kw):
return self.dispatch(args[0])(*args, **kw)
@dispatch_on_value
def react_to_param(idx): # Default function when no match.
print(f'In default react_to_param function for idx == {idx}.')
@react_to_param.register(15)
def _(idx):
print('In react_to_param function registered for idx 15')
@react_to_param.register(16)
def _(idx):
print('In react_to_param function registered for idx 16')
@react_to_param.register(17)
def _(idx):
print('In react_to_param function registered for idx 17')
# Test dispatching.
for idx in range(14, 19):
react_to_param(idx) # Only this line would go in your `_on_value_change_event()` method.
Here's the output produced that shows it works. Note that there wasn't a function registered for every possible value, in which the the default function gets called.
In default react_to_param function for idx == 14.
In react_to_param function registered for idx 15
In react_to_param function registered for idx 16
In react_to_param function registered for idx 17
In default react_to_param function for idx == 18.