继承类中定义的特殊字段的 Python 键入错误

发布于 2025-01-13 06:39:17 字数 2764 浏览 2 评论 0原文

我正在用 python 开发 Makao(澳门)纸牌游戏,并尝试在开发时使用最佳实践。 我对自己提出的挑战是使实现可扩展,因为该纸牌游戏有很多变体。 我还引入了 mypy 来检查注释。

所有当前游戏数据都存储在名为“Board”的单个类中,以下是该类的一部分:

@dataclass
class Board:
    """
    The board is the main game state dataclass.
    It stores all data about the game.
    """

    players: Players
    deck: Deck
    discard_pile: DiscardPile
    state: AbstractGameState
    player_pauses: dict = field(default_factory=dict)
    drawn_cards_in_turn: int = 0
    winners: List[Player] = field(default_factory=list)

游戏可以运行到不同的状态,例如战斗状态、正常状态、排名调用状态等。 我决定在单独的类中定义每个状态,并且 Board 类具有当前状态的对象。 它工作正常并且可扩展(如果需要,我可以轻松添加新的状态),大多数状态都定义自己的字段。例如,下面的 rank_callcall_turns 是此状态所特有的:

@dataclass
class RankCallState(AbstractGameState):
    """
    Rank call state.
    """

    rank_call: Rank
    call_turns: int
    name: str = field(default=RANK_CALL_STATE_NAME, init=False)

Board 数据类的所有更改均由操作完成。 每个操作都可以访问 Board 实例,因此也可以访问当前的 State 对象。 但在尝试从状态访问这些特定字段的操作中,mypy 抱怨 AbstractGameState 没有这些字段。 这当然是可以理解的(Board 类需要 AbstractGameState,它没有这些特殊字段),但是我如何保留扩展系统并修复打字错误的能力?

我有一种可能的解决方案(在我看来并不理想) - 我也可以将所有特殊字段放入 Board 数据类中。 如果您看到其他更好的选择来处理这个问题,我很想听听您的意见。 谢谢

编辑: 下面是一个示例操作类(我将代码行精简为重要的代码行):

@dataclass
class RankCallAction(AbstractPlayerCardAction):
    """
    Action to call a rank.
    """

    rank_call: Optional[Rank] = None

    @staticmethod
    def _check_if_card_is_valid(
        card: Card, top_card: Card, state: AbstractGameState
    ) -> bool:

        card_is_jack = card.rank == Rank.JACK
        suit_call_state_and_card_matching_jack = (
            state.name == SUIT_CALL_STATE_NAME and card.suit == state.suit_call
        )
        return card_is_jack and  suit_call_state_and_card_matching_jack
        
    @classmethod
    def get_single_cards_to_play(cls, hand: List[Card], board: Board) -> List[Card]:
        """
        Returns the single cards to play.
        :param hand: List of Card objects.
        :param board: Board object.
        :return: List of Card objects.
        """
        state = board.state
        top_card = board.discard_pile.top_card

        return [
            card for card in hand if cls._check_if_card_is_valid(card, top_card, state)
        ]

问题出在 check_if_card_is_valid 方法中,特别是 card.suit == state.suit_call - mypy Mypy 说:“AbstractGameState”没有属性“suit_call” state 取自 Board 类,因为在 Board 类中,state 字段定义为 AbstractGameState - 它导致了这个问题。

I'm working on Makao(Macao) card game in python and try to use best practices while development.
The challenge I made to myself to make the implementation extensible, because there are many variants of that card game.
I've also introduced mypy to check annotations.

All current game data is stored in single class called "Board", below is part of that class:

@dataclass
class Board:
    """
    The board is the main game state dataclass.
    It stores all data about the game.
    """

    players: Players
    deck: Deck
    discard_pile: DiscardPile
    state: AbstractGameState
    player_pauses: dict = field(default_factory=dict)
    drawn_cards_in_turn: int = 0
    winners: List[Player] = field(default_factory=list)

Game can run into different states e.g. battle state, normal state, rank call state etc.
I decided to define each of the state in separate class and Board class has object of the current state.
It works fine and is extensible (I can easily add new States if needed), most of the states define its own fields. For example below rank_call and call_turns are unique to this state:

@dataclass
class RankCallState(AbstractGameState):
    """
    Rank call state.
    """

    rank_call: Rank
    call_turns: int
    name: str = field(default=RANK_CALL_STATE_NAME, init=False)

All changes to the Board data class are being done by actions.
Each action has access to the Board instance and thus also to the current State object.
But in actions which tries to access these specific fields from the state, mypy complains that AbstractGameState does not have these fields.
This is understandable of course (Board class expects AbstractGameState which doesn't have these special fields), but how can I preserve the ability to extend the system and fix that typing errors?

I have one possible solution (not ideal in my opinion) - I can just put all the special fields in the Board data class as well.
I would like to hear from you if you see other, better options to handle that.
Thank you

EDIT:
Below is an example action class (I stripped down code lines to important ones):

@dataclass
class RankCallAction(AbstractPlayerCardAction):
    """
    Action to call a rank.
    """

    rank_call: Optional[Rank] = None

    @staticmethod
    def _check_if_card_is_valid(
        card: Card, top_card: Card, state: AbstractGameState
    ) -> bool:

        card_is_jack = card.rank == Rank.JACK
        suit_call_state_and_card_matching_jack = (
            state.name == SUIT_CALL_STATE_NAME and card.suit == state.suit_call
        )
        return card_is_jack and  suit_call_state_and_card_matching_jack
        
    @classmethod
    def get_single_cards_to_play(cls, hand: List[Card], board: Board) -> List[Card]:
        """
        Returns the single cards to play.
        :param hand: List of Card objects.
        :param board: Board object.
        :return: List of Card objects.
        """
        state = board.state
        top_card = board.discard_pile.top_card

        return [
            card for card in hand if cls._check_if_card_is_valid(card, top_card, state)
        ]

The problem is in check_if_card_is_valid method, specifically with card.suit == state.suit_call - mypy says Mypy: "AbstractGameState" has no attribute "suit_call"
The state is taken from Board class and because in Board class, state field is defined as AbstractGameState - it causes this issue.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

原谅我要高飞 2025-01-20 06:39:17

由于 Action 修改 AbstractGameState特定子类,并且我们知道需要哪个特定子类,因此对操作进行类型检查的最简单方法是将类型缩小isinstance结合使用。这使我们能够得出更精确的当前状态类型。

@staticmethod
def _check_if_card_is_valid(
    card: Card, top_card: Card, state: AbstractGameState
) -> bool:
    # We raise an error if the state is not RankCallState,
    # guaranteeing that we only proceed when the state is valid for this action
    if not isinstance(state, RankCallState):
        raise ValueError("Current state is not RankCallState")

    # Mypy now knows that `state` must be an instance of `RankCallState`
    card_is_jack = card.rank == Rank.JACK
    suit_call_state_and_card_matching_jack = (
        state.name == SUIT_CALL_STATE_NAME and card.suit == state.suit_call
    )
    return card_is_jack and  suit_call_state_and_card_matching_jack

Since Action modifies a specific subclass of AbstractGameState, and we know which specific subclass is expected, the easiest way to get the action type-checked is to use type narrowing with isinstance. This allows us to derive a more precise type of the current state.

@staticmethod
def _check_if_card_is_valid(
    card: Card, top_card: Card, state: AbstractGameState
) -> bool:
    # We raise an error if the state is not RankCallState,
    # guaranteeing that we only proceed when the state is valid for this action
    if not isinstance(state, RankCallState):
        raise ValueError("Current state is not RankCallState")

    # Mypy now knows that `state` must be an instance of `RankCallState`
    card_is_jack = card.rank == Rank.JACK
    suit_call_state_and_card_matching_jack = (
        state.name == SUIT_CALL_STATE_NAME and card.suit == state.suit_call
    )
    return card_is_jack and  suit_call_state_and_card_matching_jack
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文