Search code examples
pythonclassoopattributesinstance

How do I make my (derived?) python instance attributes "self-update" based on alterations to other same-instance attributes?


I know that I have a misunderstanding of how Python attributes work because I'm here writing this problem, but I don't know exactly what I'm misunderstanding. I'm trying to get

self.card = self.hand[self.card_number].split()
self.card_val = deck.ranks.get(self.card[0])

to attain their values based on self.hand, which I pass to __init__ upon instantiation. Throughout the game I am altering .hand, and I want .card & .card_val to change every time I change .hand, instead of having to tell it to do that elsewhere (outside of the attribute definitions). I know there is a way to do this, or at least I think there is, and by that I mean simply by defining their values as inherited based on whatever .hand is at any given time, without calling an internal or external function.

In the posted code, I have altered it to work as the game instructions require, using...

def get_card_vals(p1, p2):
    for player in [p1, p2]:
        player.card = player.hand[player.card_number].split()
        player.card_val = deck.ranks.get(player.card[0])
    print("{a} vs. {b}".format(a = p1.card, b = p2.card))
    print("---------------------------")

...but that's what I want to change. I want what that function is doing to be executed more concisely inside of the attribute definitions upon handling of the instance. Basically my question is why can't these two attributes get their values directly from the first attribute that I define via "hand" passed to the init?

Any help would be appreciated, and more importantly, I think more than just solutions, it would help me even more to understand what I am misunderstanding about how attributes, instances, and instantiation and all that works so that I know where my thinking is wrong. Thanks!

import random
from random import shuffle
from collections import deque

class Deck():
    def __init__(self):
        self.ranks = {"Ace":14, "King":13, "Queen":12, "Jack":11, "10":10, "9":9, "8":8, "7":7, "6":6, "5":5, "4":4, "3":3, "2":2}
        self.suites = ["Heart", "Diamond", "Spade", "Club"]
        self.cards = []

    def create_cards(self):
        for suite in self.suites:
            for key in self.ranks.keys():
                self.cards.append(key + " " + suite)

    def shuffle(self):
        random.shuffle(deck.cards)

deck = Deck()
deck.create_cards()
deck.shuffle()

class Player():
    def __init__(self, hand):
        self.name = "name"
        self.hand = hand
        self.card_number = 1
        self.card = self.hand[self.card_number].split()
        self.card_val = deck.ranks.get(self.card[0])

def war(bool, p1, p2):
    if bool == True:
        for player in [p1, p2]:
            player.card_number = 4
    else:
        for player in [p1, p2]:
            player.card_number = 0

p2 = Player(deque(deck.cards[::2]))
p1 = Player(deque(deck.cards[1::2]))

p2.name = "The Computer"

def get_card_vals(p1, p2):
    for player in [p1, p2]:
        player.card = player.hand[player.card_number].split()
        player.card_val = deck.ranks.get(player.card[0])
    print("{a} vs. {b}".format(a = p1.card, b = p2.card))
    print("---------------------------")

def cant_war_lose(winner, loser):
    print("{a} doesn't have enough cards to go to war, so {b} wins the Battle!".format(a = loser, b = winner))

def battle_win(winner, loser):
    print("{a} has run out of cards, therefore {b} has won via Battle!".format(a = loser, b = winner))

def play_cards(p1, p2):
    war(False, p1, p2)
    get_card_vals(p1, p2)
    if p1.card_val > p2.card_val:
        p1.hand.append(p2.hand.popleft())
        p1.hand.rotate(-1)
    elif p1.card_val == p2.card_val:
        if len(p1.hand) < 5 or len(p2.hand) < 5:
            if len(p1.hand) > len(p2.hand):
                cant_war_lose(p1.name, p2.name)
            else:
                cant_war_lose(p2.name, p1.name)
            return 0
        else:
            input("War is inititated! Press Enter to continue!")
            print("---------------------------")
            war(True, p1, p2)
            get_card_vals(p1, p2)
            if p1.card_val > p2.card_val:
                for i in range(0,5):
                    p1.hand.append(p2.hand.popleft())
                p1.hand.rotate(-5)
            elif p1.card_val < p2.card_val:
                for i in range(0,5):
                    p2.hand.append(p1.hand.popleft())
                p2.hand.rotate(-5)
            else:
                p1.hand.rotate(-1)
                p2.hand.rotate(-1)
    elif p1.card_val < p2.card_val:
        p2.hand.append(p1.hand.popleft())
        p2.hand.rotate(-1)
    if len(p1.hand) != 0 and len(p2.hand) != 0:
        input("After the last round of Battle, {a} now has {b} cards, and {c} now has {d} cards! Press Enter to continue!".format(a = p1.name, b = len(p1.hand), c = p2.name, d = len(p2.hand)))
        print("---------------------------")
    else:
        if len(p1.hand) > len(p2.hand):
            battle_win(p1.name, p2.name)
        else:
            battle_win(p2.name, p1.name)
        return 0

def game_run():
    run = 1

    p1.name = input("Player 1's name? ")
    print("---------------------------")

    while run == 1:
        if play_cards(p1, p2) == 0:
            run = 0

game_run()

Solution

  • You can use the property decorator to create a calculated property

    class Player():
        def __init__(self, hand):
            self.name = "name"
            self.hand = hand
            self.card_number = 1
    
        @property
        def hand(self):
            return self._hand
    
        @hand.setter
        def hand(self, value):
            self._hand = value
            self.card = self._hand[self.card_number].split()
            self.card_val = deck.ranks.get(self.card[0])