1

I am building a simple card game using classes and encountering something confusing.

To start with here are the relevant methods of my 'Deck' class.

class Deck(object):
    def __init__(self, starting_cards=[]):
        self._cards = starting_cards

    def get_amount(self):
        return len(self._cards)

    def add_card(self, card):
        self._cards.append(card)

And here is part of my 'Player' class:

class Player(object):
    def __init__(self, name):
        self._name = name
        self._hand = Deck()
        self._coders = Deck()

    def get_hand(self):
        return self._hand

    def get_coders(self):
        return self._coders

    def has_won(self):
        if self._coders.get_amount() >= 4:
            return True
        else:
            return False

There are a few 'Card' sub-classes but they are irrelevant to this question, the following code should add 3 cards to self._hand, yet it adds the cards to both self._hand and self._coders, which are two completely different instances of the Deck() class.

player = Player("Lochie Deakin-Sharpe")
player.get_hand().add_card(NumberCard(3))
player.get_hand().add_card(KeyboardKidnapperCard())
player.get_hand().add_card(AllNighterCard())
player.get_hand() 

Which part of my code is adding these cards to self._coders, which are far as I can tell is not called?

After running the above code here are some commands:

>>> player.get_coders()
Deck(NumberCard(3), KeyboardKidnapperCard(), AllNighterCard())

>>> player.get_hand().add_card(4)
>>> player.get_hand()
Deck(NumberCard(3), KeyboardKidnapperCard(), AllNighterCard(), 4)

>>> player.get_coders()
Deck(NumberCard(3), KeyboardKidnapperCard(), AllNighterCard(), 4)
  • Interesting read, I can't seem to make the link to my issue as I am not calling the self._coders instance in any of my code. – engstudent296 Sep 03 '19 at 01:53
  • 1
    It answers your question precisely - all your instances of `Deck` uses the same list object. But looking in your current setup, why make it an arg at all? Just make an empty list inside your class constructor instead. – Henry Yik Sep 03 '19 at 02:02
  • You have the default `list` object for all your `Deck` objects. they are all *using the same list* – juanpa.arrivillaga Sep 03 '19 at 02:35

1 Answers1

2

It looks like your Player class has two references of the same _cards list.

In Python, you have to be very careful about passing in lists as default parameters. To see what I mean, run the code below after defining your classes:

p = Player('Me')
print('Memory Address of Hand: 0x{}'.format(id(p.get_hand()._cards)))
print('Memory Address of Coders: 0x{}'.format(id(p.get_coders()._cards)))

Notice how the memory address of both the hand list and the coder list is the same. That is because of the tricky behavior of passing the default list from your parameters directly to your class property.

To avoid this behavior, add the following bit to your Deck init function:

def __init__(self, starting_cards=None):
    if starting_cards is None:
        starting_cards = []
    self._cards = starting_cards

Instead of passing the empty list directly from your default parameters, we are going to set starting_cards to None. If we find that a starting cards list was not passed, instantiate an empty list, which will then be assigned to the _cards property. By doing so, you avoid the multiple references made to the same array pointer.

This is a common Python gotcha. If you are interested in learning more about it, this article touches on the concept well: https://docs.python-guide.org/writing/gotchas/

Also, Raymond Hettinger did a wonderful talk on the nuances of Python class development. The video is from 2013, but it still covers many of the concepts necessary to make Pythonic classes in Python 3: https://www.youtube.com/watch?v=HTLu2DFOdTg

For example, it is best to use @property instead of explicit getters and setters in Python classes, which could help in your classes' performance and readability over time.