如何用Python实现可动态扩展的YAML问卷?
问题:哪种Python实现方案更适合处理YAML问卷?
需求背景
需要实现Python类处理包含70余个问题的YAML问卷,核心要求如下:
- 读取YAML格式的问卷内容
- 支持动态扩展(比如新增问题类型)
- 代码具备良好可读性
- 处理用户输入的数据验证
YAML问卷示例
- question: "What is your name?" type: text required: true - question: "How old are you?" type: number required: true min_value: 18 max_value: 100 - question: "What is your gender?" type: multiple_choice choices: - Male - Female - Other required: true - question: "Do you have any dietary restrictions?" type: checkboxes choices: - Vegetarian - Vegan - Gluten-free - Dairy-free - None - question: "Which programming languages do you know?" type: checkboxes choices: - Python - JavaScript - Java - C++ - Ruby - question: "How satisfied are you with our product?" type: scale min_value: 1 max_value: 5 - question: "Any additional comments or feedback?" type: textarea
两种实现方案
方案一
from enum import Enum import yaml class QuestionKind(Enum): TEXT = "text" NUMBER = "number" MULTIPLE_CHOICE = "multiple_choice" CHECKBOXES = "checkboxes" SCALE = "scale" TEXTAREA = "textarea" class Question: def __init__(self, prompt, kind, choices=None): self.prompt = prompt self.kind = kind self.choices = choices class Quiz: def __init__(self, questions_file): self.questions = self.load_questions(questions_file) self.current_index = 0 def load_questions(self, questions_file): with open(questions_file, "r") as file: data = yaml.safe_load(file) questions = [] for item in data: prompt = item["question"] kind = QuestionKind(item["type"]) choices = item.get("choices") question = Question(prompt, kind, choices) questions.append(question) return questions def current_question(self): if self.current_index < len(self.questions): return self.questions[self.current_index] else: return None def provide_answer(self, answer): self.current_index += 1 # Process the answer as needed return self.current_question() # Usage example quiz = Quiz("questions.yaml") current_question = quiz.current_question() while current_question is not None: print("Question:", current_question.prompt) answer = input("Your answer: ") current_question = quiz.provide_answer(answer)
方案二
import sys from pathlib import Path import ruamel.yaml file_in = Path('questions.yaml') print(file_in.read_text(), end='===============\n') class BaseQuestion: def __init__(self, question, required=False, conditions=None): self._question = question self._required = required self._value = None # to store user response to question self._conditions = conditions or [] @classmethod def from_yaml(cls, constructor, node): kw = ruamel.yaml.CommentedMap() constructor.construct_mapping(node, kw) return cls(**kw) def check_conditions(self, responses): for condition in self._conditions: question_id = condition.get('id') operator = condition.get('operator') value = condition.get('value') if question_id in responses and self._compare_values(responses[question_id], operator, value): return False return True def _compare_values(self, value1, operator, value2): """Compare two values based on the given operator""" if operator == "==": return value1 == value2 elif operator == "!=": return value1 != value2 elif operator == ">": return value1 > value2 elif operator == "<": return value1 < value2 elif operator == ">=": return value1 >= value2 elif operator == "<=": return value1 <= value2 else: raise ValueError(f"Invalid operator: {operator}") def __repr__(self): return f'{self.yaml_tag}("{self._question}", required={self._required}, conditions={self._conditions})' class BaseTextQuestion(BaseQuestion): yaml_tag = '!Text' def __init__(self, question, required=False, conditions=None): super().__init__(question=question, required=required, conditions=conditions) class TextQuestion(BaseTextQuestion): yaml_tag = '!Text' class TextAreaQuestion(BaseTextQuestion): yaml_tag = '!TextArea' class NumberQuestion(BaseQuestion): yaml_tag = '!Number' def __init__(self, question, min_value, max_value, required=False, conditions=None): super().__init__(question=question, required=required, conditions=conditions) self._min = min_value self._max = max_value def check(self, responses): if not self.check_conditions(responses): return False if self._required and self._value is None: return False if self._value is None: return True return self._min <= self._value <= self._max def __repr__(self): return f'{self.yaml_tag}("{self._question}", range=[{self._min}, {self._max}], required={self._required}, conditions={self._conditions})' class ScaleQuestion(NumberQuestion): yaml_tag = '!Scale' class ChoiceQuestion(BaseQuestion): def __init__(self, question, choices, required=False, conditions=None): super().__init__(question=question, required=required, conditions=conditions) self._choices = choices def __repr__(self): return f'{self.yaml_tag}("{self._question}", choices=[{", ".join(self._choices)}], required={self._required}, conditions={self._conditions})' class MultipleChoiceQuestion(ChoiceQuestion): yaml_tag = '!MultipleChoice' class CheckBoxesQuestion(ChoiceQuestion): yaml_tag = '!CheckBoxes' yaml = ruamel.yaml.YAML() yaml.register_class(TextQuestion) yaml.register_class(TextAreaQuestion) yaml.register_class(NumberQuestion) yaml.register_class(ScaleQuestion) yaml.register_class(MultipleChoiceQuestion) yaml.register_class(CheckBoxesQuestion) questions = yaml.load(file_in) def display_questions(questions): """Display the questions based on the conditions and user responses""" responses = {} for question in questions: if question.check_conditions(responses): response = input(question._question + " ") responses[question.__dict__.get("_id")] = response print("User Responses:", responses) display_questions(questions)
方案对比与选择
方案一优缺点
- 优点:结构简单,代码量少,入门门槛低,用Enum统一管理问题类型,逻辑直观。
- 缺点:完全缺失输入验证逻辑,没有处理
required、min_value等问卷字段;动态扩展需要修改Enum和Question类,后续维护会越来越臃肿;单个Question类承担所有问题类型的属性,不符合单一职责原则,面对70+问题的规模,代码会变得难以维护。
方案二优缺点
- 优点:
- 模块化设计:采用类继承体系,每个问题类型对应独立子类,职责单一,可读性极强,新增问题类型只需继承对应父类即可,完美支持动态扩展。
- 内置验证逻辑:每个子类实现了对应的数据验证(比如NumberQuestion的范围检查、必填项校验),直接匹配问卷中的
required、min_value等字段。 - 条件支持:内置了问题显示的条件判断逻辑,能实现问卷的分支流程,适合复杂问卷场景。
- YAML映射清晰:通过ruamel.yaml的自定义标签实现YAML字段到Python类的直接映射,处理大规模问卷时逻辑更清晰。
- 缺点:需要熟悉ruamel.yaml的自定义标签用法,学习成本略高于方案一,但对于长期维护来说完全值得。
最终结论
方案二更适合处理这个需求。它完美覆盖了所有核心要求:读取YAML内容、支持动态扩展、代码可读性强、内置输入验证,尤其是面对70+问题的规模,方案二的模块化设计能大幅降低后续维护和扩展的成本。
内容的提问来源于stack exchange,提问作者ProxilityProblemSolver
相关产品推荐
相关产品推荐

