如何在Python数据类中复用属性代码消除重复样板代码?
问题
我是个新手,正在把自己写的Kahoot生成器改成能在GitHub分享的开源库。希望用户能用简单的=赋值操作设置测验信息,比如quiz.title = "XYZ"。但Kahoot的自定义文本有字符限制,得验证输入是否超限。目前我在KahootQuiz数据类里用自定义setter/getter实现验证,但每个属性都要写大量重复的样板代码,太麻烦了。
我试过两种方法都不行:
- 封装
KahootText类处理验证逻辑,在KahootQuiz的__post_init__里初始化,但运行时重新赋值会把属性变成字符串,覆盖类实例; - 用工厂函数生成属性,但调试时没法方便查看属性的最大长度等信息。
我需要一个不影响用户使用体验、支持嵌套(比如带字符限制的测验答案列表)的解决方案,预期用户使用示例如下:
from kahoot import KahootQuiz, KahootBlock from Constants import block, time, points if __name__ == "__main__": """ TESTING CODE """ quiz = KahootQuiz( blocks=[ KahootBlock( block_type=block.MULTIPLE_CHOICE, title="Which is correct?", answers=["Right", "Wrong", "wrong"], correct_answer=0, point_type=points.DOUBLE_POINTS, time_limit=time.THIRTY_SECONDS ) ], settings=KahootSettings( general=KahootGeneralSettings(title="Testy test Quiz", description="Random nonesense blahblahblah") ) ) print(quiz.blocks[0].title) # Expected Output: "Which is correct?" print(quiz.settings.general.title) # Expected Output: Testy test Quiz quiz.settings.general.make_link_public = True # Should work quiz.blocks[0].answers[1] = "INCORRECT" # Should work, but keep the KahootText assignment quiz.questions[0].correct_answer = 10 # Should fail, as the answers list isn't that long quiz.blocks[0].title = "LOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOONNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNG TITLE" # Should fail, as it's too long to fit in KahootText quiz.questions[0].answers[2] = "LOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOONNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNG ANSWER" # Should also fail, as it's too long to fit in Kahoottext quiz.questions[0].answers.append("A new incorrect answer!") # Should work quiz.questions[0].answers.append("Another new incorrect answer!") # Should fail, as answers is now too big to fit in a Kahoot quiz.point_type = Constants.NO_POINTS # Should work quiz.point_type = 2 # Should also work, but give a warning because it's not using the expected class quiz.point_type = Constants.time.FIVE_SECONDS # Should also work, but give a warning because it's not using the expected class quiz.point_type = 100000000000 # Should fail quiz.point_type = Constants.time.FOUR_MINUTES # should also fail, since it's out of index of the class quiz.blocks[0].media = KahootImage( media="path/to/an/image.jpg", # Replace with actual image path alt_text="An image on some person's computer", credit_source="Image by whoever is running this script" ) # Should run, as the media section of a quiz block accepts both images and video quiz.blocks[0].media = KahootVideo( media="path/to/an/image.mov", # Replace with actual image path alt_text="A video on some person's computer", credit_source="Video by whoever is running this script", start=KahootTimeStamp.from_string("00:00") ) # Should run, as the media section of a quiz block accepts both images and video print(quiz)
解决方案
1. 用描述符封装文本验证逻辑
描述符能把属性的验证逻辑抽离复用,还能保留调试时的可查看性(比如直接看到最大长度)。
class KahootTextDescriptor: def __init__(self, max_length: int): self.max_length = max_length self._name = "" def __set_name__(self, owner, name): self._name = f"_{name}" def __get__(self, instance, owner): if instance is None: return self return getattr(instance, self._name, "") def __set__(self, instance, value): if not isinstance(value, str): raise TypeError(f"{self._name.strip('_')}必须是字符串类型") if len(value) > self.max_length: raise ValueError(f"{self._name.strip('_')}超出最大长度限制:{self.max_length}字符") setattr(instance, self._name, value)
在数据类里直接使用这个描述符:
from dataclasses import dataclass @dataclass class KahootGeneralSettings: title = KahootTextDescriptor(max_length=100) # 对应Kahoot标题的实际限制 description = KahootTextDescriptor(max_length=500) make_link_public: bool = False def __post_init__(self): # 初始化时验证传入的参数 if hasattr(self, '_title'): self.title = self._title if hasattr(self, '_description'): self.description = self._description
2. 自定义容器类处理嵌套列表(如答案列表)
针对需要验证元素长度和列表最大数量的场景,继承list重载核心方法:
class KahootAnswerList(list): def __init__(self, max_item_length: int, max_total_items: int, initial_items=None): self.max_item_length = max_item_length self.max_total_items = max_total_items initial_items = initial_items or [] for item in initial_items: self.append(item) def __setitem__(self, index, value): if len(value) > self.max_item_length: raise ValueError(f"答案超出最大长度限制:{self.max_item_length}字符") super().__setitem__(index, value) def append(self, value): if len(self) >= self.max_total_items: raise ValueError(f"最多只能添加{self.max_total_items}个答案") if len(value) > self.max_item_length: raise ValueError(f"答案超出最大长度限制:{self.max_item_length}字符") super().append(value)
在KahootBlock中集成这个容器:
@dataclass class KahootBlock: block_type: int title = KahootTextDescriptor(max_length=200) answers: KahootAnswerList = None correct_answer: int = 0 point_type: int = 0 time_limit: int = 0 _media = None def __post_init__(self): # 初始化答案列表 if self.answers is None: self.answers = KahootAnswerList(max_item_length=100, max_total_items=4) elif isinstance(self.answers, list): self.answers = KahootAnswerList(max_item_length=100, max_total_items=4, initial_items=self.answers) # 验证正确答案的索引范围 if self.correct_answer < 0 or self.correct_answer >= len(self.answers): raise IndexError(f"正确答案索引{self.correct_answer}超出范围")
3. 枚举+属性setter处理枚举值(如point_type)
用枚举定义合法值,同时兼容整数输入并给出警告:
from enum import Enum import warnings class Points(Enum): NO_POINTS = 0 STANDARD_POINTS = 1 DOUBLE_POINTS = 2 class KahootBlock: # ... 其他属性 ... _point_type: int = 0 @property def point_type(self): return self._point_type @point_type.setter def point_type(self, value): valid_values = [p.value for p in Points] if isinstance(value, Points): self._point_type = value.value elif isinstance(value, int): if value not in valid_values: if value > max(valid_values) or value < min(valid_values): raise ValueError(f"无效的point_type值{value},必须是{valid_values}中的一个") warnings.warn(f"直接使用整数{value}设置point_type,建议使用Points枚举类", UserWarning) self._point_type = value else: raise TypeError("point_type必须是Points枚举或整数类型")
4. 多类型兼容属性(如media)
通过属性setter支持多种合法类型:
class KahootImage: def __init__(self, media: str, alt_text: str, credit_source: str): self.media = media self.alt_text = alt_text self.credit_source = credit_source class KahootVideo: def __init__(self, media: str, alt_text: str, credit_source: str, start): self.media = media self.alt_text = alt_text self.credit_source = credit_source self.start = start class KahootBlock: # ... 其他属性 ... _media = None @property def media(self): return self._media @media.setter def media(self, value): if isinstance(value, (KahootImage, KahootVideo)): self._media = value else: raise TypeError("media必须是KahootImage或KahootVideo实例")
最终效果
完全符合你预期的用户体验:
- 用
=直接赋值时自动触发验证 - 嵌套属性(如
quiz.settings.general.title)和列表操作(如answers[1] = "INCORRECT")都能正常验证 - 调试时可直接查看描述符的
max_length、容器类的max_item_length等参数 - 非法输入会抛出明确的错误或警告
内容的提问来源于stack exchange,提问作者Jediweirdo
相关产品推荐
相关产品推荐

