You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 21:19:51