为配置屏幕中的每个设置调用不同方法的 Pythonic 方法

Pythonic way to call different methods for each setting in a configuration screen

我目前正在为我的应用程序设计配置屏幕。大约有50个配置参数,我已经在CSV文件中描述过,读入Python.

目前每个参数行的字段是:

因此,参数的几个示例可能是:

然后在我的应用程序中绘制每个参数作为 UI 供用户交互,看起来像这样:

csv_row_count 稍后用作循环的 idx。如果我想添加另一个要配置的参数,我将不得不使用一个唯一的参数并将该参数添加到 CSV 的底部。

我目前有一个 class,它将所有信息存储到属性中,并且可以在用户更改后存储回 CSV。

现在,我遇到的问题是我需要对用户在配置屏幕中所做的更改做出反应(每个参数都是唯一的)。

所以我的想法是创建一个 ConfigEventHandler class,它有一个 Event 用于创建参数(在应用程序启动时,将其绘制到 UI 例如)和一个 Event 当值被改变时触发(改变 UI 中的值,并且也对这个已经改变的特定设置做出相应的反应).

class 看起来像这样:

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))

我在创建 class 和值发生变化时触发这些事件。此事件识别哪个值已更改的唯一方法是通过与 csv_row_count 属性相关的 idx 值,如 CSV 中所述。

这意味着如果我想对每一个被改变的参数做出反应,我需要用大量的 if 语句填充 _on_value_change_event 方法,例如:

if idx == 0:
    react_to_param0()
elif idx == 1:
    react_to_param1()
etc...

我觉得这真的是不好的做法(在任何程序语言中),如果有人在 CSV 文件中乱用 idx 编号(例如几个月后的我,或者将接管我的项目的人)。

所以我的问题是,以一种比很多 if 语句和列表 idx 之间的关系更干净的方式来解决这个关系问题的好的替代方法是什么?和 CSV 索引,使其更适合未来,并保持代码的可读性?

我曾考虑过为每个可配置参数创建一个单独的 class 实例,但并没有真正看到这将如何打破 csv_row_count/[ 之间看似 'fragile' 的关系=25=] 以及对一个特定参数的变化做出反应的独特功能。

你应该使用比 CSV 更好的东西。例如,一个 YAML 文件。 https://tutswiki.com/read-write-yaml-config-file-in-python/

一个可能的解决方案是使用一个字典属性来存储函数名称,如下所示:

def __init__(self):
    self.params_reactions = {
        0: self.react_to_param1,  # Each value is a reference of a function in the class
        1: self.react_to_param2,
        2: self.react_to_param3
        # Continues...
    }

然后如果索引存在并且与字典的键匹配,你就可以调用反应函数:

def _on_value_change_event(self, idx, user_dict):
    # Your code as well

    if idx in self.params_reactions:
        self.params_reactions[idx].__call__()

    # You could raise an exception if 'idx' is not found
    # It depends on how you want to treat a possible exception to the program's flow

以“pythonic”方式做事通常涉及使用字典,这是该语言的主要数据结构之一,已针对速度和最小内存使用量进行了高度优化。

考虑到这一点,我要做的第一件事就是将 CSV 文件中的配置参数整合为一个。由于 CSV 文件的每一行都有一个,合乎逻辑的做法是使用 csv.DictReader 来读取它们,因为它 returns 每行作为值字典。通过使用每个字典的唯一 csv_row_count(又名 idx)字段作为上层 dict.[=24 的键,您可以轻松地从这些字典中构建字典=]

我的意思是:

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)

因此,如果您的 CSV 文件包含如下行:

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

用上面的代码读取它会导致构建以下字典中的字典:

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'}}

函数的调度

现在,就对每个参数变化做出反应而言,一种方法是使用函数装饰器来定义何时根据其值调用装饰器所应用的函数第一个参数。这将消除在您的 _on_value_change_event() 方法中使用一长串 if 语句的需要——因为它可以用对命名装饰函数的单个调用来替换。

我从文章 Learn About Python Decorators by Writing a Function Dispatcher 中得到实现装饰器的想法,尽管我使用 class 以稍微不同的方式实现了它(而不是嵌套函数)功能),我认为这有点“清洁”。还要注意装饰器本身如何使用名为 registry 的内部字典来存储有关信息并进行实际调度——另一种“pythonic”方式来做事。

再一次,这就是我的意思:

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.

这是显示它有效的输出。请注意,并没有为每个可能的值注册一个函数,其中调用了默认函数。

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.