This chapter is taken from the book A Primer on Scientific Programming with Python by H. P. Langtangen, 5th edition, Springer, 2016.
Suppose we want to draw a random integer among the values 1, 2, 3, and 4, and that each of the four values is equally probable. One possibility is to draw real numbers from the uniform distribution on, e.g., \( [0,1) \) and divide this interval into four equal subintervals:
import random
r = random.random()
if 0 <=  r < 0.25:
    r = 1
elif 0.25 <= r < 0.5:
    r = 2
elif 0.5 <= r < 0.75:
    r = 3
else:
    r = 4
Nevertheless, the need for drawing uniformly distributed integers occurs quite frequently, so there are special functions for returning random integers in a specified interval \( [a, b] \).
Python's random module has a built-in function
randint(a,b) for drawing an integer in \( [a,b] \), i.e.,
the return value is among the numbers
a, a+1, \( \ldots \), b-1, b.
import random
r = random.randint(a, b)
The numpy.random.randint(a, b, N) function has a similar
functionality for vectorized drawing of an array of length N of
random integers in \( [a, b) \).  The upper limit b is not among the
drawn numbers, so if we want to draw from a, a+1, \( \ldots \), b-1,
b, we must write
import numpy as np
r = np.random.randint(a, b+1, N)
Another function, random_integers(a, b, N), also in numpy.random,
includes the upper limit b in the possible set of random integers:
r = np.random.random_integers(a, b, N)
We can make a function that lets the computer
throw a die N times and returns the fraction of the throws
the die shows six eyes:
def six_eyes(N):
    M = 0                  # no of times we get 6 eyes
    for i in xrange(N):
        outcome = random.randint(1, 6)
        if outcome == 6:
            M += 1
    return float(M)/N
We use xrange instead of range because the former avoids
storing N numbers in memory, which can be an important feature
when N is large.
Too speed up the experiments, we can vectorize the drawing of the random numbers and the counting of the number successful experiments:
import numpy as np
def six_eyes_vec(N):
    eyes = np.random.randint(1, 7, N)
    success = eyes == 6     # True/False array
    M = np.sum(success)     # treats True as 1, False as 0
    return float(M)/N
The eyes == 6 construction results in an array with True
or False values, and np.sum applied to this array treats
True as 1 and False as 0 (the integer equivalents to the
boolean values), so the sum is the number of elements in eyes
that equals 6. A very important point here for computational
efficiency is to use np.sum and not the standard
sum function that is available in standard Python.
With np.sum function, the vectorized version runs about
60 times faster than the scalar version. With Python's standard
sum function, the vectorized versions is in fact twice as slow
as the scalar version (!). We can illustrate the gain and loss
in efficiency by the follow IPython session:
In [1]: from roll_die import six_eyes, six_eyes_vec
In [2]: %timeit six_eyes(100000)
1 loops, best of 3: 250 ms per loop
In [3]: %timeit six_eyes_vec(100000)
100 loops, best of 3: 4.11 ms per loop
In [4]: 250/4.11           # performance fraction
Out[4]: 60.8272506082725
In [5]: from roll_die import np
In [6]: np.sum = sum  # fool numpy to use built-in Python sum
In [7]: %timeit six_eyes_vec(100000)
1 loops, best of 3: 543 ms per loop
(Note how we can bind Python's built-in sum function to the
np.sum name such that np.sum in six_eyes_vec applies
Python's sum function instead of the original np.sum.)
The disadvantage with the vectorized version is that all the random
numbers must be stored in the computer's memory. A large N may cause
the program to run out of memory and raise a MemoryError.
Instead of drawing all random numbers at once, we can draw them
in batches of size arraysize. There will be N//arraysize such
batches, plus a rest. Note the double slash in N//arraysize: here
we indeed want integer division, which is explicitly instructed in
Python by the double forward slash. The rest is obtained by the
mod operator: rest = N % arraysize. The size of the batches can
be stored in a list:
rest = N % arraysize
batch_sizes = [arraysize]*(N//arraysize) + [rest]
We can now make one batch of random numbers at a time and count how many times we get six:
def six_eyes_vec2(N, arraysize=1000000):
    # Split all experiments into batches of size arraysize,
    # plus a final batch of size rest
    # (note: N//arraysize is integer division)
    rest = N % arraysize
    batch_sizes = [arraysize]*(N//arraysize) + [rest]
    M = 0
    for batch_size in batch_sizes:
        eyes = np.random.randint(1, 7, batch_size)
        success = eyes == 6      # True/False array
        M += np.sum(success)     # treats True as 1, False as 0
    return float(M)/N
Because we fix the seed, the computed f will always be the same in
this function.
Verifying computations with random numbers requires the seed to be
fixed. When we believe the scalar version in function six_eyes is
correct, mainly by observing that the return value approaches 1/6
as the number of experiments, N, grows, we can call the function
with a small N and record the return value for use in a test function:
def test_scalar():
    random.seed(3)
    f = six_eyes(100)
    f_exact = 0.26
    assert abs(f_exact - f) < 1E-15
Since we have three alternative functions for computing the same
quantity, a verification can be based on comparing the output of
all three functions. This is somewhat problematic since the scalar
and vectorized versions apply different random number generators.
Fixing the seed of Python's random module and numpy.random
does not help as these two tools will generate different sequences
of random integers. Nevertheless, we can fool the scalar version
in the six_eyes function to use np.random.randint instead
of random.randit: this is just a matter of setting random = np.random
(with a declaration global random) before calling six_eyes.
The problem is that the call np.random.randint(1, 6) in six_eyes
will then generate the numbers up to but not including 6, so
M will always be zero. A little trick, can solve the problem:
we redefine random.randit to be a function that calls
np.random.randint:
random.randint = lambda a, b: np.random.randint(a, b+1, 1)[0]
The call random.randint(1, 6) in six_eyes now becomes
np.random.randint(1, 7, 1)[0], i.e., we generate an array of
1 random integer and extract the first element so the result is
a scalar number as before.
A test function can call all three functions, with the same fixed seed, and compare the returned values:
def test_all():
    # Use np.random as random number generator for all three
    # functions and make sure all of them applies the same seed
    N = 100
    arraysize = 40
    random.randint = lambda a, b: np.random.randint(a, b+1, 1)[0]
    tol = 1E-15
    np.random.seed(3)
    f_scalar = six_eyes(N)
    np.random.seed(3)
    f_vec = six_eyes_vec(N)
    assert abs(f_scalar - f_vec) < tol
    np.random.seed(3)
    f_vec2 = six_eyes_vec2(N, arraysize=80)
    assert abs(f_vec - f_vec2) < tol
All the functions above are found in the file roll_die.py.
Given a list a, the statement
re = random.choice(a)
picks out an element of a at random, and re refers to
this element.
The shown call to random.choice is the same as
re = a[random.randint(0, len(a)-1)]
There is also a function shuffle that performs a random permutation
of the list elements:
random.shuffle(a)
Picking now a[0], for instance, has the same effect as
random.choice on the original, unshuffled list.
Note that shuffle changes the list given as argument.
The numpy.random module has also a shuffle function with
the same functionality.
A small session illustrates the various methods for picking a random element from a list:
>>> awards = ['car', 'computer', 'ball', 'pen']
>>> import random
>>> random.choice(awards)
'car'
>>> awards[random.randint(0, len(awards)-1)]
'pen'
>>> random.shuffle(awards)
>>> awards[0]
'computer'
The following function creates a deck of cards, where each card is represented as a string, and the deck is a list of such strings:
def make_deck():
    ranks = ['A', '2', '3', '4', '5', '6', '7',
             '8', '9', '10', 'J', 'Q', 'K']
    suits = ['C', 'D', 'H', 'S']
    deck = []
    for s in suits:
        for r in ranks:
            deck.append(s + r)
    random.shuffle(deck)
    return deck
Here, 'A' means an ace, 'J' represents a jack, 'Q'
represents a queen, 'K' represents a king,
'C' stands for clubs, 'D' stands for diamonds,
'H' means hearts, and 'S' means spades.
The computation of the list deck
can alternatively (and more compactly)
be done by a one-line list comprehension:
    deck = [s+r for s in suits for r in ranks]
We can draw a card at random by
deck = make_deck()
card = deck[0]
del deck[0]
# or better:
card = deck.pop(0)  # return and remove element with index 0
Drawing a hand of n cards from a shuffled deck is accomplished by
def deal_hand(n, deck):
    hand = [deck[i] for i in range(n)]
    del deck[:n]
    return hand, deck
Note that we must return deck to the calling code since this
list is changed. Also note that the n first cards of the deck
are random cards if the deck is shuffled (and any deck made by
make_deck is shuffled).
The following function deals cards to a set of players:
def deal(cards_per_hand, no_of_players):
    deck = make_deck()
    hands = []
    for i in range(no_of_players):
        hand, deck = deal_hand(cards_per_hand, deck)
        hands.append(hand)
    return hands
players = deal(5, 4)
import pprint; pprint.pprint(players)
The players list may look like
[['D4', 'CQ', 'H10', 'DK', 'CK'], ['D7', 'D6', 'SJ', 'S4', 'C5'], ['C3', 'DQ', 'S3', 'C9', 'DJ'], ['H6', 'H9', 'C6', 'D5', 'S6']]
The next step is to analyze a hand. Of particular interest is the
number of pairs, three of a kind, four of a kind, etc. That is,
how many combinations there are of n_of_a_kind
cards of the same rank (e.g., n_of_a_kind=2 finds the number of
pairs):
def same_rank(hand, n_of_a_kind):
    ranks = [card[1:] for card in hand]
    counter = 0
    already_counted = []
    for rank in ranks:
        if rank not in already_counted and \ 
               ranks.count(rank) == n_of_a_kind:
            counter += 1
            already_counted.append(rank)
    return counter
Note how convenient the count method in list objects is for counting
how many copies there are of one element in the list.
Another analysis of the hand is to count how many cards there are of each suit. A dictionary with the suit as key and the number of cards with that suit as value, seems appropriate to return. We pay attention only to suits that occur more than once:
def same_suit(hand):
    suits = [card[0] for card in hand]
    counter = {}   # counter[suit] = how many cards of suit
    for suit in suits:
        count = suits.count(suit)
        if count > 1:
            counter[suit] = count
    return counter
For a set of players we can now analyze their hands:
for hand in players:
    print """\ 
The hand %s
    has %d pairs, %s 3-of-a-kind and %s cards of the same suit."""%\ 
    (', '.join(hand), same_rank(hand, 2),
     same_rank(hand, 3),
     '+'.join([str(s) for s in same_suit(hand).values()]))
The values we feed into the printf string undergo some massage:
we join the card values with comma and put a plus in between the
counts of cards with the same suit. (The join function
requires a string
argument. That is why the integer counters of cards with the
same suit, returned from same_suit, must be converted to
strings.)
The output of the for loop becomes
The hand D4, CQ, H10, DK, CK has 1 pairs, 0 3-of-a-kind and 2+2 cards of the same suit. The hand D7, D6, SJ, S4, C5 has 0 pairs, 0 3-of-a-kind and 2+2 cards of the same suit. The hand C3, DQ, S3, C9, DJ has 1 pairs, 0 3-of-a-kind and 2+2 cards of the same suit. The hand H6, H9, C6, D5, S6 has 0 pairs, 1 3-of-a-kind and 2 cards of the same suit.
The file cards.py contains the
functions make_deck, hand, same_rank, same_suit, and the test
snippets above. With the cards.py file one can start to implement
real card games.
To work with a deck of cards with the code from the previous section
one needs to shuffle a global variable deck in and out of
functions. A set of functions that update global variables (like deck) is
a primary candidate for a class: the global variables are stored as
data attributes and the functions become class methods.
This means that the code from the previous section is better implemented
as a class. We introduce class Deck with a list of cards, deck,
as data attribute, and methods for dealing one or several hands and for
putting back a card:
class Deck(object):
    def __init__(self):
        ranks = ['A', '2', '3', '4', '5', '6', '7',
                 '8', '9', '10', 'J', 'Q', 'K']
        suits = ['C', 'D', 'H', 'S']
        self.deck = [s+r for s in suits for r in ranks]
        random.shuffle(self.deck)
    def hand(self, n=1):
        """Deal n cards. Return hand as list."""
        hand = [self.deck[i] for i in range(n)]  # pick cards
        del self.deck[:n]                        # remove cards
        return hand
    def deal(self, cards_per_hand, no_of_players):
        """Deal no_of_players hands. Return list of lists."""
        return [self.hand(cards_per_hand) \ 
                for i in range(no_of_players)]
    def putback(self, card):
        """Put back a card under the rest."""
        self.deck.append(card)
    def __str__(self):
        return str(self.deck)
This class is found in the module file Deck.py.  Dealing a hand of five cards to p
players is coded as
from Deck import Deck
deck = Deck()
print deck
players = deck.deal(5, 4)
Here, players become a nested list as shown in
the section Example: Drawing cards from a deck.
One can go a step further and make more classes for assisting card
games. For example, a card has so far been represented by a plain string,
but we may well put that string in a class Card:
class Card(object):
    """Representation of a card as a string (suit+rank)."""
    def __init__(self, suit, rank):
        self.card = suit + str(rank)
    def __str__(self):   return self.card
    def __repr__(self):  return str(self)
Note that str(self) is equivalent to self.__str__().
A Hand contains a set of Card instances and
is another natural abstraction, and hence a candidate
for a class:
class Hand(object):
    """Representation of a hand as a list of Card objects."""
    def __init__(self, list_of_cards):
        self.hand = list_of_cards
    def __str__(self):   return str(self.hand)
    def __repr__(self):  return str(self)
With the aid of classes Card and Hand, class Deck
can be reimplemented as
class Deck(object):
    """Representation of a deck as a list of Card objects."""
    def __init__(self):
        ranks = ['A', '2', '3', '4', '5', '6', '7',
                 '8', '9', '10', 'J', 'Q', 'K']
        suits = ['C', 'D', 'H', 'S']
        self.deck = [Card(s,r) for s in suits for r in ranks]
        random.shuffle(self.deck)
    def hand(self, n=1):
        """Deal n cards. Return hand as a Hand object."""
        hand = Hand([self.deck[i] for i in range(n)])
        del self.deck[:n]         # remove cards
        return hand
    def deal(self, cards_per_hand, no_of_players):
        """Deal no_of_players hands. Return list of Hand obj."""
        return [self.hand(cards_per_hand) \ 
                for i in range(no_of_players)]
    def putback(self, card):
        """Put back a card under the rest."""
        self.deck.append(card)
    def __str__(self):
        return str(self.deck)
    def __repr__(self):
        return str(self)
    def __len__(self):
        return len(self.deck)
The module file Deck2.py
contains this implementation. The usage
of the two Deck classes is the same,
from Deck2 import Deck
deck = Deck()
players = deck.deal(5, 4)
with the exception that players in the last case holds a list
of Hand instances, and each Hand instance holds a list of
Card instances.
The golden rule is
that the
__repr__ method should return a string such that
one can recreate the object from this string by the aid of eval.
However, we did not follow this rule in the implementation of
classes Card, Hand, and Deck. Why?
The reason is that we want to print a Deck instance.
Python's print or pprint on a list applies repr(e)
to print an element e in the list.
Therefore, if we had implemented
class Card(object):
    ...
    def __repr__(self):
        return "Card('%s', %s)" % (self.card[0], self.card[1:])
class Hand(object):
    ...
    def __repr__(self):  return 'Hand(%s)' % repr(self.hand)
a plain printing of the deck list of Hand instances
would lead to output like
[Hand([Card('C', '10'), Card('C', '4'), Card('H', 'K'), ...]), ..., Hand([Card('D', '7'), Card('C', '5'), ..., Card('D', '9')])]
This output is harder to read than
[[C10, C4, HK, DQ, HQ], [SA, S8, H3, H10, C2], [HJ, C7, S2, CQ, DK], [D7, C5, DJ, S3, D9]]
That is why we let __repr__ in classes Card and Hand
return the same pretty print string
as __str__, obtained by returning str(self).