TL;DR __repr__
should help us recreate an object even if it has random behaviour or if it changes itself during runtime. Check the final Deck Class.
>>> deck1 = Deck(5) # Unseeded Deck 1 without extra behaviour
[0, 3, 4, 2, 1]
>>> deck2 = Deck(5) # Unseeded Deck 2 without extra behaviour, different from Deck 1
[3, 4, 1, 2, 0]
>>> repr(deck1) # Deck 1's current state
Deck(5, 777611, 0)
>>> repr(deck2) # Deck 2's current state
Deck(5, 849477, 0)
>>> deck1.draw() # Draws a card from Deck 1
1
>>> deck1.draw() # Draws another card from Deck 1
2
>>> print(deck1)
[0, 3, 4]
>>> repr(deck1) # Deck 1's current state
Deck(5, 777611, 2)
>>> Deck(5, 777611, 2) # Seeded Deck 3 with Deck 1's extra behaviour
[0, 3, 4]
Picture this:
>>> deck = Deck(5) # A new randomly shuffled deck
[2, 4, 0, 1, 3]
>>> deck.draw() # Draws a new card from the deck (simply pops an element)
3
>>> deck.draw()
1
>>> deck
[2, 4, 0]
What if we could freeze this object to recreate it later? The python built-in __repr__
function is what we need, so let's try it out:
>>> repr(deck)
<__main__.Deck object at 0x7fbcddf70650>
Uh oh... we didn't implement it yet, so let's do it!
This is our Deck
class:
from random import shuffle
class Deck(object):
def __init__(self, size):
self._deck = list(range(0, size))
shuffle(self._deck)
self._size = size
def draw(self):
try:
card = self._deck.pop()
except IndexError:
card = None
return card
def __str__(self):
return str(self._deck)
We can create a new Deck
object and print it, but we can't get it's repr
just yet:
>>> deck = Deck(5)
>>> print(deck)
[3, 1, 0, 2, 4]
>>> repr(deck)
<__main__.Deck object at 0x7fbe376d45d0>
So we need to override the repr
function:
def __repr__(self):
name = self.__class__.__name__
return '{}({})'.format(name, self._size)
Now, if we run the same code again:
>>> deck = Deck(5)
>>> print(deck)
[0, 4, 3, 1, 2]
>>> repr(deck)
Deck(5)
It's stating that if we run Deck(5)
we'll get a new object equal to the previous one, which is not true:
>>> print(Deck(5)) # Deck 1
[2, 4, 1, 0, 3]
>>> print(Deck(5)) # Deck 2, different from Deck 1 even though it's using exactly the same parameter
[3, 0, 4, 1, 2]
The shuffling randomness doesn't allow us to simply recreate this object. For that, we need to plant a seed.
Let's import seed
and add it to our initializer:
from random import shuffle, seed # Import seed
class Deck(object):
def __init__(self, size):
self._deck = list(range(0, size))
seed(13) # Now we control the randomness
shuffle(self._deck)
self._size = size
Our __repr__
is finally working as intended and Deck(5)
will recreate equal objects every time:
>>> Deck(5) # Deck 1
[0, 4, 3, 2, 1]
>>> Deck(5) # Deck 2, equal to Deck 1
[0, 4, 3, 2, 1]
>>> Deck(5) # Deck 3, equal to Deck 1
[0, 4, 3, 2, 1]
Unfortunately, it means we lose our randomness. We'll get the SAME shuffled deck every time which isn't good. Let's bring the randomness back whilst being able to recreate an specific object:
from random import shuffle, seed
from datetime import datetime as dt # We'll use the time to randomly generate our seed
class Deck(object):
def __init__(self, size, new_seed=None): # Add new_seed to __init__
self._deck = list(range(0, size))
# If seed is empty, generate a new one
# Otherwise, use the given seed
if not new_seed:
new_seed = dt.now().microsecond
seed(new_seed)
self._seed = new_seed
shuffle(self._deck)
self._size = size
def __repr__(self):
name = self.__class__.__name__
return '{}({}, {})'.format(name, self._size, self._seed) # Add seed number to __repr__
Now we can create new randomly shuffled decks, whilst being able to completely recreate a previous deck:
>>> deck = Deck(5) # Unseeded Deck 1
>>> print(deck)
[0, 3, 4, 2, 1]
>>> repr(deck)
Deck(5, 777611)
>>> Deck(5) # Unseeded Deck 2, different from Deck 1
[3, 2, 4, 0, 1]
>>> Deck(5, 777611) # Seeded Deck 3, new Deck object but shuffled the same way as Deck 1
[0, 3, 4, 2, 1]
But what happens if we draw some cards and call __repr__
again?
Let's draw some cards:
>>> deck = Deck(5, 777611) # Seeded Deck 1
>>> print(deck)
[0, 3, 4, 2, 1]
>>> repr(deck)
Deck(5, 777611)
>>> deck.draw()
1
>>> deck.draw()
2
>>> print(deck) # Deck 1 with some cards already drawn
[0, 3, 4]
>>> repr(deck) # Still representing Deck 1's initial state
Deck(5, 777611)
Again, our __repr__
isn't representing our object's current state. Deck(5, 777611)
would recreate the initial state of the object but we need it's current state.
We could change our __repr__
to return the object's current size, but it would create a different object:
>>> deck = Deck(5, 777611) # Seeded Deck 1
>>> deck.draw()
1
>>> deck.draw()
2
>>> print(deck)
[0, 3, 4]
>>> repr(deck) # Deck 1 with less cards
Deck(3, 777611)
>>> Deck(3, 777611) # Deck 2, different from Deck 1. Not cool, Deck!
[2, 1, 0]
See how both objects have different cards? We need to tell our initializer how to behave in order to recreate that object:
class Deck(object):
def __init__(self, size, new_seed=None, keep_drawing=0): # Add keep_drawing to __init__
self._deck = list(range(0, size))
if not new_seed:
new_seed = dt.now().microsecond
seed(new_seed)
self._seed = new_seed
shuffle(self._deck)
self._size = size
self._drawn = 0 # Keep record of how many cards were drawn
# Tells the initializer how to behave
while keep_drawing:
# Draw as many cards as stated
self.draw()
keep_drawing -= 1
def draw(self):
try:
card = self._deck.pop()
self._drawn += 1 # Increment number of drawn cards
except IndexError:
card = None
return card
def __repr__(self):
name = self.__class__.__name__
return '{}({}, {}, {})'.format(name, self._size, self._seed, self._drawn) # Add _drawn number to __repr__
Now if we run our code again:
>>> deck = Deck(5, 777611) # Seeded Deck 1 without extra behaviour
>>> repr(deck)
Deck(5, 777611, 0)
>>> deck.draw()
1
>>> deck.draw()
2
>>> print(deck)
[0, 3, 4]
>>> repr(deck) # Deck 1's current state
Deck(5, 777611, 2)
>>> Deck(5, 777611, 2) # Seeded Deck 2 with Deck 1's extra behaviour
[0, 3, 4]
Now we can finally commit and push to production :)