Skip to content

Instantly share code, notes, and snippets.

@arthurazs
Last active February 8, 2018 02:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arthurazs/2a9e7ec13cbc18edc19195fb2f224622 to your computer and use it in GitHub Desktop.
Save arthurazs/2a9e7ec13cbc18edc19195fb2f224622 to your computer and use it in GitHub Desktop.
__repr__ function with randomness and object behaviour in Python

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]

__repr__ function

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!

Our class

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.

Seeding the randomness

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?

Object behaviour

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 :)

from random import shuffle, seed
from datetime import datetime as dt
class Deck(object):
def __init__(self, size, new_seed=None, keep_drawing=0):
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
while keep_drawing:
self.draw()
keep_drawing -= 1
def draw(self):
try:
card = self._deck.pop()
self._drawn += 1
except IndexError:
card = None
return card
def __str__(self):
return str(self._deck)
def __repr__(self):
name = self.__class__.__name__
return '{}({}, {}, {})'.format(name, self._size, self._seed, self._drawn)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment