Part 4 - playing poker


Today's subject matter is perfect for something fun. We'll create a poker game in Python. This will help us learn about a new data type and more along the way.
Before we begin, I have to confess I don't play poker. When I started the project, I had only a vague idea about cowboys betting money in old Western films. As you progress, you'll see that programming is often as much about coding as it is about learning new things. Programs should fit their problem set, meaning you must first become familiar with it. So let's learn about our domain.
As I searched Wikipedia for poker rulesets, I got a vague idea of what I should code first. A common feature of poker games is the hands. Hands are card combinations. Below is a list in decreasing order (the first hands are the best, the last are the worst.)
Royal Flush: the highest hand. A, K, Q, J, and 10 in the same suit.
Straight Flush: five consecutive cards in the same suite, e.g., 7-8-9-10-J of hearts.
Four of a Kind: four cards of the same rank, and any fifth card, e.g., four kings and a 2 of spades.
Full House: three cards of the same rank (e.g., three queens) plus a pair (e.g., two sevens).
Flush: any five cards of the same suit, not in sequence, e.g., 2-3-5-8-A of hearts.
Straight: five consecutive cards of mixed suits, e.g., 3-4-5-6-7.
Three of a Kind: three cards of the same rank, e.g., three jacks.
Two Pairs: two distinct pairs, like two 3s and two queens.
One Pair: one single pair of cards, e.g., two 6s.
High Card: if you don't have a better hand, take your highest-ranked card.
A good idea would be to code a ranker for a hand of cards. We can plug it into different poker rules or gameplays later. I want to create a reusable component.
Cards, hands, and decks
Before we create our hand ranker, we need to be able to create ranks, suits, cards, hands, and card decks.
We can store ranks and suits in simple, basic lists like this:
RANKS = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
SUITS = ["hearts", "diamonds", "clubs", "spades"]
I chose to define the lists of possible suit and rank values as constants (note the capital letters). They will be global constants, available to our whole program. This is easy and suitable for such a small program.
Let’s also create two heper functions to get the rank and the suit of a card.
def get_rank(card):
return card[0]
def get_suit(card):
return card[1]
Because Python does not have ready-made card, hand, and deck types, we will need to "expand" the language. This is so we can talk to Python in the language of card games rather than in the language of computers. It is clearer to say, "Tell me the rank of this card" instead of "Return the first element of this tuple." The code will be easier to read, maintain, and change. Adapting a language to better suit a problem is a powerful programming concept.
There are two main ways of accommodating new kinds of data in programming.
The direct approach. This focuses on the data itself. In some languages, you might use a 'struct' (short for structure). Python uses classes for new types of data. Classes are part of object-oriented programming (OOP). I'll show you OOP in detail in a future post.
The indirect approach. We don't spend much time defining the actual data structure. Instead, we define some operations to manipulate it. We will use this approach in our current post because it is simpler.
Here's what those two approaches would look like in real life if you wanted to tell all about cards to your buddy. (note: this is an exaggeration.) On the one hand, you could tell him that a card is
a piece of thin cardstock, with a coating layer for durability
of a rectangular shape with rounded corners
with dimensions approx. 2.5 inches by 3.5 inches
with a front and a back
the front containing
a suit symbol (♥️, ♣️, ♠️, ♦️)
a value: a number from 2-10 or a face (Jack, Queen, King, or Ace)
the back contains some logo or a pattern
you can play games with the cards
etc, etc
This is focusing on the data.
Or you could tell him he can use a card like this:
shuffling the deck
dealing a card
drawing a card from the top of the deck
checking the card's rank
checking the card's suit
holding your cards in your hand
flipping a card face-up
etc, etc
You get the idea. The indirect approach is more natural here. It requires less code, and it's easier to understand. Also, we can focus on functionality instead of structure.
After this digression, let's head back to our problem.
First, what are some characteristics of a card deck?
A deck should have the cards in random order (like when we shuffle a real-world card deck).
Moreover, it should be easy to deal cards from the deck.
A card shouldn't be present more than once in the deck.
Sets
Sets are the perfect solution for us. Let's look at the code, and I'll explain it point by point.
def make_deck():
deck = set()
for rank in RANKS:
for suit in SUITS:
card = (rank, suit)
deck.add(card)
return deck
# Create
my_deck = make_deck()
print(my_deck)
You define an empty set with set()
. So every time we call make_deck
, there's a new variable, deck
, internal to make_deck
. It holds an empty set. We will populate it so we can return it full of cards at the end of the function.
Our function loops over each possible combination of suits and ranks. For each combination, it creates a tuple (rank, suit)
and adds it to the deck.
We used tuples for cards because a set's elements must be immutable. We can change the contents of lists and dictionaries. Defining cards as a list (e.g., [rank, suit]
) or as a dictionary (e.g., {'rank': rank, 'suit: suit}
) would have created an error.
Sets are collections. They are 'unordered,' meaning they hold their elements in no particular order. Those elements are unique; Python will remove any duplicates. Sets can contain only immutable elements, like numbers, strings, and tuples. You define sets either with curly braces, {}
, or set()
.
my_set = {1, 2, 3}
my_empty_set = set()
The elements are unique, so you can do this:
>>> my_set = {1, 2, 2, 3}
>>> print(my_set)
{1, 2, 3}
Since set elements have no order, we can do this {1, 2, 3} == {2, 3, 1}
and get a True
answer. Try it!
The add
method adds a new element to a set. Since elements must be unique, adding an item to a set where it's already present gives you the original set. add
doesn't return anything. It changes the set, and that's it.
The method we use for shuffling card decks is a cheat. If you enter a set in your Python REPL and print it several times, it will appear in the same order.
>>> my_set = {('a', 1), ('b', 2), 3}
>>> print(my_set)
{3, ('b', 2), ('a', 1)}
>>> print(my_set)
{3, ('b', 2), ('a', 1)}
>>> print(my_set)
{3, ('b', 2), ('a', 1)}
The order of the set's elements may appear random, and it's enough for our use case, but it's not random. It's a property of how Python stores set data under the hood. It gives each element a unique code (called a 'hash') to determine its place in memory. Don't worry about the details for now. It makes sets fast.
Each time we launch our program, the deck has the same data, but its internal structure is different. That's because the hashing function is different each time we run our program. So the decks appear shuffled.
But if we built two different decks with make_deck
in the same run, the order of the elements would be stable. Because Python reused the same hashing function.
When we say sets are unordered, we really mean Python doesn't care about element order. And neither should you. You also can't access set elements by their index. my_set[0]
gives an error.
We could have used true shuffling, but it's part of the random
module. It would not have worked on sets, we would have needed a list. The random module offers a few useful functions to deal with randomness. I want to introduce modules and randomness later, so I didn't use this solution.
For now, our 'cheat' works well enough. Let me prove it to you with some program output.
% python3 Project4.py
{('3', 'hearts'), ('8', 'clubs'), ('Q', 'clubs'), ('2', 'diamonds'), ('J', 'diamonds'), ('10', 'diamonds'), ('7', 'spades'), ('9', 'clubs'), ('A', 'clubs'), ('2', 'spades'), ('A', 'hearts'), ('5', 'clubs'), ('8', 'hearts'), ('10', 'spades'), ('Q', 'hearts'), ('J', 'spades'), ('6', 'clubs'), ('3', 'diamonds'), ('9', 'hearts'), ('5', 'hearts'), ('3', 'spades'), ('8', 'diamonds'), ('4', 'clubs'), ('7', 'clubs'), ('Q', 'diamonds'), ('6', 'hearts'), ('A', 'diamonds'), ('K', 'clubs'), ('8', 'spades'), ('A', 'spades'), ('5', 'diamonds'), ('9', 'diamonds'), ('4', 'hearts'), ('Q', 'spades'), ('2', 'clubs'), ('6', 'diamonds'), ('7', 'hearts'), ('10', 'clubs'), ('K', 'hearts'), ('5', 'spades'), ('9', 'spades'), ('J', 'clubs'), ('4', 'diamonds'), ('6', 'spades'), ('10', 'hearts'), ('3', 'clubs'), ('7', 'diamonds'), ('2', 'hearts'), ('J', 'hearts'), ('K', 'diamonds'), ('4', 'spades'), ('K', 'spades')}
% python3 Project4.py
{('Q', 'hearts'), ('Q', 'clubs'), ('A', 'diamonds'), ('10', 'hearts'), ('J', 'spades'), ('10', 'clubs'), ('9', 'diamonds'), ('K', 'diamonds'), ('8', 'diamonds'), ('Q', 'spades'), ('5', 'diamonds'), ('A', 'hearts'), ('A', 'clubs'), ('4', 'diamonds'), ('10', 'spades'), ('K', 'hearts'), ('K', 'clubs'), ('9', 'hearts'), ('8', 'hearts'), ('9', 'clubs'), ('5', 'hearts'), ('4', 'hearts'), ('7', 'diamonds'), ('4', 'clubs'), ('8', 'clubs'), ('5', 'clubs'), ('A', 'spades'), ('2', 'diamonds'), ('K', 'spades'), ('6', 'diamonds'), ('7', 'hearts'), ('9', 'spades'), ('3', 'diamonds'), ('8', 'spades'), ('5', 'spades'), ('2', 'hearts'), ('7', 'clubs'), ('2', 'clubs'), ('4', 'spades'), ('J', 'diamonds'), ('6', 'hearts'), ('3', 'hearts'), ('6', 'clubs'), ('7', 'spades'), ('3', 'clubs'), ('Q', 'diamonds'), ('2', 'spades'), ('J', 'hearts'), ('J', 'clubs'), ('6', 'spades'), ('10', 'diamonds'), ('3', 'spades')}
Our program can create a new deck every time we run it. Now, let's create a function to distribute a hand of cards. Since I wanted to rate hands of 5 cards next, I hard-coded the number of cards (5).
def deal_hand(deck):
hand = []
for i in range(0, 5):
card = deck.pop()
hand.append(card)
return hand
# Testing code
my_deck = make_deck()
print(f"deck length: {len(my_deck)}\ndeck pre: {my_deck}")
my_hand = deal_hand(my_deck)
print(f"hand: {my_hand}")
print(f"deck length: {len(my_deck)}\ndeck post: {my_deck}")
Hands are lists of cards in our example. This will make analyzing hands easier when we get to it, as we'll be able to order them. deal_hand
'pops' card off the deck. It removes an arbitrary element from the set and returns it. So, the set shrinks as we build our hand. This mirrors real life, where we deal cards from a deck to create a hand. pop
doesn't let you specify which element you wish to remove, as it takes no arguments.
Let’s look at the program’s output. It will
create a deck
show it’s length
print the deck
deal a hand
print the hand
print the new deck length
reprint the deck
% python3 Project4.py
deck length: 52
deck pre: {('K', 'hearts'), ('6', 'clubs'), ('7', 'diamonds'), ('7', 'hearts'), ('A', 'clubs'), ('A', 'spades'), ('5', 'clubs'), ('5', 'spades'), ('10', 'spades'), ('J', 'clubs'), ('J', 'spades'), ('8', 'spades'), ('10', 'clubs'), ('K', 'spades'), ('8', 'clubs'), ('K', 'clubs'), ('7', 'clubs'), ('7', 'spades'), ('4', 'diamonds'), ('4', 'hearts'), ('K', 'diamonds'), ('Q', 'spades'), ('4', 'clubs'), ('4', 'spades'), ('2', 'diamonds'), ('2', 'hearts'), ('A', 'hearts'), ('9', 'diamonds'), ('9', 'hearts'), ('2', 'spades'), ('9', 'spades'), ('2', 'clubs'), ('9', 'clubs'), ('3', 'diamonds'), ('3', 'hearts'), ('Q', 'diamonds'), ('Q', 'hearts'), ('J', 'hearts'), ('6', 'diamonds'), ('6', 'hearts'), ('3', 'spades'), ('5', 'diamonds'), ('5', 'hearts'), ('Q', 'clubs'), ('3', 'clubs'), ('A', 'diamonds'), ('10', 'diamonds'), ('10', 'hearts'), ('J', 'diamonds'), ('6', 'spades'), ('8', 'diamonds'), ('8', 'hearts')}
hand: [('K', 'hearts'), ('6', 'clubs'), ('7', 'diamonds'), ('7', 'hearts'), ('A', 'clubs')]
deck length: 47
deck post: {('A', 'spades'), ('5', 'clubs'), ('5', 'spades'), ('10', 'spades'), ('J', 'clubs'), ('J', 'spades'), ('8', 'spades'), ('10', 'clubs'), ('K', 'spades'), ('8', 'clubs'), ('K', 'clubs'), ('7', 'clubs'), ('7', 'spades'), ('4', 'diamonds'), ('4', 'hearts'), ('K', 'diamonds'), ('Q', 'spades'), ('4', 'clubs'), ('4', 'spades'), ('2', 'diamonds'), ('2', 'hearts'), ('A', 'hearts'), ('9', 'diamonds'), ('9', 'hearts'), ('2', 'spades'), ('9', 'spades'), ('2', 'clubs'), ('9', 'clubs'), ('3', 'diamonds'), ('3', 'hearts'), ('Q', 'diamonds'), ('Q', 'hearts'), ('J', 'hearts'), ('6', 'diamonds'), ('6', 'hearts'), ('3', 'spades'), ('5', 'diamonds'), ('5', 'hearts'), ('Q', 'clubs'), ('3', 'clubs'), ('A', 'diamonds'), ('10', 'diamonds'), ('10', 'hearts'), ('J', 'diamonds'), ('6', 'spades'), ('8', 'diamonds'), ('8', 'hearts')}
The hand ranker
Now we can write our hand ranker. We could have done it before. But now we have a set of useful functions to debug the program as we write it.
Royal flush
To check if a hand is a royal flush, we must
Check if the ranks are 10, J, Q, K, and A.
Check if the suits are all the same
Below is a naive solution, along with the test code. The royal_flush
function goes through each card of a hand, checks the rank, and then checks the suit.
def royal_flush(hand):
"""
10, jack, queen, king, ace, all of the same suit
"""
possible_ranks = {'10', 'J', 'Q', 'K', 'A'}
current_suit = None
for card in hand:
card_rank = get_rank(card)
if card_rank in possible_ranks:
possible_ranks.remove(card_rank)
else:
return False
card_suit = get_suit(card)
if current_suit:
if card_suit != current_suit:
return False
else:
current_suit = card_suit
return True
Here is some code to test our function. test_card_royal_flush_T
is a royal flush, while the other hand is not. We test our royal_flush
function on each hand and print the resulting Boolean on the screen.
test_card_royal_flush_T = [
('10', 'clubs'),
('Q', 'clubs'),
('J', 'clubs'),
('K', 'clubs'),
('A', 'clubs')
]
test_card_royal_flush_F = [
('2', 'diamonds'),
('9', 'clubs'),
('J', 'clubs'),
('4', 'hearts'),
('A', 'spades')
]
print(royal_flush(test_card_royal_flush_T))
print(royal_flush(test_card_royal_flush_F))
% python3 Project4.py
True
False
royal_flush
is way too long. Can you think of a way to shorten it? You'll see an improved version soon enough.
Sorting hands with a helper function
Before we move on, I'll need the ability to sort cards by rank. For example, we will need to check if cards are in ascending order, detect pairs of cards with the same rank, etc.
I wrote a short helper function for this.
def sort_hand(hand):
return sorted(hand, key=get_rank)
What does key=get_rank
mean?
Remember part I of this series, when we saw dispatch tables? We used functions as values in a dictionary. Here, we use a function, get_rank
, as an argument to another function. This shows you can manipulate functions like any other object in Python. We say functions are first-class objects. Being able to pass functions as parameters to a function is a very powerful feature.
sorted
is a built-in Python function. It accepts as parameters 1/ a list and a 2/ function that tells it how to sort the list's items. The sorting function is a keyword argument: you enter it in the form function name(<kwarg>=<arg>)
. Here, the name of the parameter is key
, and the parameter's value is the get_rank
function.
sorted
will call the get_rank
function on each card to get a sorting value. This value decides the sorting order of the cards in the hand. The key function does not alter the hand or its elements, it's only here to decide the sorting order.
By changing the sorting parameter, we can sort the cards in all sorts of ways. Can you tell what sorted(cards, key=get_suit)
does?
Straight flush
straight_flush
will be trickier than royal flush
. Now, we deal with more than one sequence of possible ranks.
def straight_flush(hand):
"""
five cards of the same suit in sequential order.
"""
sorted_hand = sort_hand(hand)
rank_list = [get_rank(card) for card in sorted_hand]
if not str(rank_list)[1:-1] in str(RANKS)[1:-1]:
return False
current_suit = None
for card in sorted_hand:
card_suit = get_suit(card)
if current_suit:
if card_suit != current_suit:
return False
else:
current_suit = card_suit
return True
Let's unpack what this means.
A new Python feature in this code is a list comprehension: [get_rank(card) for card in sorted_hand]
. We can translate this to "Make a list out of the ranks of each card in sorted_hand
." List comprehensions are one way to create lists. They can shorten code and make it easier to understand. The example above is the same as
rank_list = []
for card in sorted_hand:
rank_list.append(get_rank(card))
Now let's unpack str(rank_list)[1:-1] in str(RANKS)[1:-1]
. The main point is we are dealing with substrings. We want to know if the list of sorted ranks appears as is in RANKS
. First, we turn rank_list
and RANKS
into strings. For example, rank_list
could be ['2', '3', '4', '5', '6']
. Then, str(rank_list)
would be "['2', '3', '4', '5', '6']"
.
str(RANKS)
is "['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']"
. We don't care one bit about the square brackets, so we ditch them with [1:-1]
. We then look if one string is a subset of the other with the in
keyword.
This is very brittle. Turning lists into strings and ditching their brackets as characters is a dirty hack. We'll refactor the code later to improve it.
But first let's test if it runs as we intended.
straight_flush_sorted_hand = [('2', 'hearts'), ('3', 'hearts'), ('4', 'hearts'), ('5', 'hearts'), ('6', 'hearts')]
straight_flush_hand = [('K', 'clubs'), ('J', 'clubs'), ('9', 'clubs'), ('Q', 'clubs'), ('10', 'clubs')]
my_deck = make_deck()
random_hand = deal_hand(my_deck)
print(f"hand 1: {straight_flush_sorted_hand}\nSTRAIGHT FLUSH ? {straight_flush(straight_flush_sorted_hand)}")
print(f"hand 2: {straight_flush_hand}\nSORTED: {sort_hand(straight_flush_hand)}\nSTRAIGHT FLUSH ? {straight_flush(straight_flush_hand)}")
print(f"hand 3: {random_hand}\nSTRAIGHT FLUSH ? {straight_flush(random_hand)}")
% python3 Project4.py
hand 1: [('2', 'hearts'), ('3', 'hearts'), ('4', 'hearts'), ('5', 'hearts'), ('6', 'hearts')]
STRAIGHT FLUSH ? True
hand 2: [('K', 'clubs'), ('J', 'clubs'), ('9', 'clubs'), ('Q', 'clubs'), ('10', 'clubs')]
SORTED: [('10', 'clubs'), ('9', 'clubs'), ('J', 'clubs'), ('K', 'clubs'), ('Q', 'clubs')]
STRAIGHT FLUSH ? False
hand 3: [('9', 'diamonds'), ('3', 'diamonds'), ('5', 'hearts'), ('4', 'clubs'), ('K', 'clubs')]
STRAIGHT FLUSH ? False
We get False
for straight flushes. This is because we were careless when we designed sort_hand
. It works well if the ranks are numbers but falls short when J, Q, K, or A come in.
sort_hand redesign
We'll still use the sorted function. But we want to provide another sorting function.
def sort_hand(hand):
return sorted(hand, key=lambda card: RANKS.index(get_rank(card)))
Do you see the lambda
keyword? It's a way to create anonymous function definitions. We could have written
def helper_function(card):
return RANKS.index(get_rank(card))
def sort_hand(hand):
return sorted(hand, key=helper_function)
The advantage of using a lambda is inserting one-liner functions inline in your code. You don't have to bother with writing a separate function. This can make the code cleaner. Only use an anonymous function if it gets called at exactly one place in the code. If you have the same code at several locations, putting it inside its own function is a good idea.
The syntax of lambdas is
lambda arguments: expression
This is exactly the same as
def my_function(arguments):
return expression
That was your first brush with lambdas. Congrats! As I write more posts in this tutorial, you'll see many more of them.
Now, back to our sorting function. Can you work out what it does? (Hint: if you don't remember what index
does, we talked about it in Post 2.)
That's right, our sorting function sorts a hand by matching each card's rank with its index in RANKS
. For example, '2' would get index 0, "J" would get index 9, and "K" would get rank 11. So, we're still sorting based on rank in a way, but now we can be sure the ordering's right. Let's write some testing code and check if straight_flush
works now.
straight_flush_sorted_hand = [('2', 'hearts'), ('3', 'hearts'), ('4', 'hearts'), ('5', 'hearts'), ('6', 'hearts')]
straight_flush_hand = [('K', 'clubs'), ('J', 'clubs'), ('9', 'clubs'), ('Q', 'clubs'), ('10', 'clubs')]
my_deck = make_deck()
random_hand = deal_hand(my_deck)
print(f"hand 1: {straight_flush_sorted_hand}\nSTRAIGHT FLUSH ? {straight_flush(straight_flush_sorted_hand)}")
print(f"hand 2: {straight_flush_hand}\nSORTED: {sort_hand(straight_flush_hand)}\nSTRAIGHT FLUSH ? {straight_flush(straight_flush_hand)}")
print(f"hand 3: {random_hand}\nSTRAIGHT FLUSH ? {straight_flush(random_hand)}")
% python3 Project4.py
hand 1: [('2', 'hearts'), ('3', 'hearts'), ('4', 'hearts'), ('5', 'hearts'), ('6', 'hearts')]
STRAIGHT FLUSH ? True
hand 2: [('K', 'clubs'), ('J', 'clubs'), ('9', 'clubs'), ('Q', 'clubs'), ('10', 'clubs')]
SORTED: [('9', 'clubs'), ('10', 'clubs'), ('J', 'clubs'), ('Q', 'clubs'), ('K', 'clubs')]
STRAIGHT FLUSH ? True
hand 3: [('9', 'spades'), ('5', 'spades'), ('3', 'hearts'), ('A', 'clubs'), ('J', 'spades')]
STRAIGHT FLUSH ? False
Much better.
Four of a kind
def four_of_a_kind(hand):
"""
Any four cards of the same rank
"""
rank_list = [get_rank(card) for card in hand]
for rank in RANKS:
if rank_list.count(rank) == 4:
return True
return False
Look at rank_list
. It is a list comprehension like the one in straight_flush
. Without checking the explanation, can you read the list definition?
The function works by going through each possible rank (for rank in RANKS
). For each of these ranks, it checks if it appears 4 times in the hand (rank_list.count(rank) == 4
).
We already learned of count
in Post 2. Go review that section if you don't remember it.
Let's test our code:
four_of_a_kind_hand = [('2', 'hearts'), ('2', 'clubs'), ('2', 'diamonds'), ('2', 'spades'), ('6', 'hearts')]
random_hand = [('9', 'spades'), ('5', 'spades'), ('3', 'hearts'), ('A', 'clubs'), ('J', 'spades')]
print(f"4 of a kind hand: {four_of_a_kind(four_of_a_kind_hand)}")
print(f"random hand: {four_of_a_kind(random_hand)}")
% python3 Project4.py
4 of a kind hand: True
random hand: False
Full House
The principle is roughly the same as with four_of_a_kind
. We will go through each possible rank and check if there are either three or two cards of this rank in our hand. We'll store the results in boolean variables. Try to write the function on your own first. You can test it with this code:
full_house_hand = [('A', 'hearts'), ('7', 'clubs'), ('A', 'diamonds'), ('A', 'spades'), ('7', 'hearts')]
my_deck = make_deck()
random_hand = deal_hand(my_deck)
print(f"4 of a kind hand: {full_house(full_house_hand)}")
print(f"random hand: {random_hand}\n\t{full_house(random_hand)}")
Here's the expected output:
% python3 Project4.py
4 of a kind hand: True
random hand: [('6', 'clubs'), ('10', 'hearts'), ('3', 'spades'), ('Q', 'diamonds'), ('9', 'hearts')]
False
Got it? Here's how I wrote it.
def full_house(hand):
"""
Three of a kind and a pair
"""
pair = False
triple= False
rank_list = [get_rank(card) for card in hand]
for rank in RANKS:
if rank_list.count(rank) == 3:
triple = True
elif rank_list.count(rank) == 2:
pair = True
return pair and triple
Flush
Try to write flush
yourself. It will check if all the cards have the same suit. You can use a similar approach to four_of_a_kind
, but using SUITS
instead of RANKS
. Test your idea with this code:
flush_hand = [('3', 'diamonds'), ('8', 'diamonds'), ('6', 'diamonds'), ('K', 'diamonds'), ('10', 'diamonds')]
my_deck = make_deck()
random_hand = deal_hand(my_deck)
print(f"flush hand: {flush(flush_hand)}")
print(f"random hand: {random_hand}\n\t{flush(random_hand)}")
Here's the expected output:
% python3 Project4.py
flush hand: True
random hand: [('4', 'hearts'), ('4', 'spades'), ('5', 'spades'), ('6', 'clubs'), ('2', 'hearts')]
False
Here's my take on it.
def flush(hand):
"""
Five cards of the same suit.
"""
suit_list = [get_suit(card) for card in hand]
for suit in SUITS:
if suit_list.count(suit) == 5:
return True
return False
Straight
You should also be able to write this one on your own. Take inspiration from straight_flush
. As usual, here's some test code:
straight_sorted_hand = [('2', 'hearts'), ('3', 'clubs'), ('4', 'hearts'), ('5', 'diamondss'), ('6', 'spades')]
straight_hand = [('K', 'clubs'), ('J', 'spades'), ('9', 'clubs'), ('Q', 'diamonds'), ('10', 'clubs')]
my_deck = make_deck()
random_hand = deal_hand(my_deck)
print(f"hand 1: {straight_sorted_hand}\nSTRAIGHT ? {straight(straight_sorted_hand)}")
print(f"hand 2: {straight_hand}\nSORTED: {sort_hand(straight_hand)}\nSTRAIGHT ? {straight(straight_hand)}")
print(f"hand 3: {random_hand}\nSTRAIGHT ? {straight(random_hand)}")
the expected output:
% python3 Project4.py
hand 1: [('2', 'hearts'), ('3', 'clubs'), ('4', 'hearts'), ('5', 'diamondss'), ('6', 'spades')]
STRAIGHT ? True
hand 2: [('K', 'clubs'), ('J', 'spades'), ('9', 'clubs'), ('Q', 'diamonds'), ('10', 'clubs')]
SORTED: [('9', 'clubs'), ('10', 'clubs'), ('J', 'spades'), ('Q', 'diamonds'), ('K', 'clubs')]
STRAIGHT ? True
hand 3: [('5', 'diamonds'), ('6', 'diamonds'), ('A', 'hearts'), ('3', 'clubs'), ('10', 'hearts')]
STRAIGHT ? False
and my own code:
def straight(hand):
"""
Five cards of any suits, in sequential order.
"""
sorted_hand = sort_hand(hand)
rank_list = [get_rank(card) for card in sorted_hand]
if not str(rank_list)[1:-1] in str(RANKS)[1:-1]:
return False
return True
Three of a kind
You should feel familiar with this by now.
Test code:
three_of_a_kind_hand = [('2', 'hearts'), ('7', 'clubs'), ('2', 'diamonds'), ('2', 'spades'), ('6', 'hearts')]
random_hand = [('9', 'spades'), ('5', 'spades'), ('3', 'hearts'), ('A', 'clubs'), ('J', 'spades')]
print(f"3 of a kind hand: {three_of_a_kind(three_of_a_kind_hand)}")
print(f"random hand: {three_of_a_kind(random_hand)}")
Output:
% python3 Project4.py
3 of a kind hand: True
random hand: False
My code:
def three_of_a_kind(hand):
"""
Any three cards of the same rank
"""
rank_list = [get_rank(card) for card in hand]
for rank in RANKS:
if rank_list.count(rank) == 3:
return True
return False
Two pairs
Same principle, but add a counter to check if you have two pairs. A useful operator to use is the increment operator. It adds a number to a variable. var += val
adds val
to var
. For example, pairs += 1
adds 1 to the variable pairs
.
Test code:
two_pair_hand = [('2', 'hearts'), ('7', 'clubs'), ('2', 'diamonds'), ('7', 'spades'), ('6', 'hearts')]
random_hand = [('9', 'spades'), ('5', 'spades'), ('3', 'hearts'), ('A', 'clubs'), ('J', 'spades')]
print(f"2 pair hand: {two_pair(two_pair_hand)}")
print(f"random hand: {two_pair(random_hand)}")
Output:
% python3 Project4.py
2 pair hand: True
random hand: False
My code:
def two_pair(hand):
"""
Two different pairs in the same hand.
"""
pairs = 0
rank_list = [get_rank(card) for card in hand]
for rank in RANKS:
if rank_list.count(rank) == 2:
pairs += 1
return pairs == 2
One pair
Test code:
one_pair_hand = [('2', 'hearts'), ('6', 'clubs'), ('2', 'diamonds'), ('7', 'spades'), ('K', 'hearts')]
random_hand = [('9', 'spades'), ('5', 'spades'), ('3', 'hearts'), ('A', 'clubs'), ('J', 'spades')]
print(f"1 pair hand: {one_pair(one_pair_hand)}")
print(f"random hand: {one_pair(random_hand)}")
Output:
% python3 Project4.py
1 pair hand: True
random hand: False
My code:
def one_pair(hand):
"""
Any two numerically matching cards.
"""
rank_list = [get_rank(card) for card in hand]
for rank in RANKS:
if rank_list.count(rank) == 2:
return True
return False
High rank
We want our function to return a number corresponding to the highest card in our hand. 2 is the lowest and should return 2, while A is the highest and should return 14. We could start the points at 0 or 1. It doesn't matter. As usual, try to write the function by yourself. Remember, we already wrote a sort_hand
function. It will come in handy.
Here's the test code:
my_deck = make_deck()
hand_1 = deal_hand(my_deck)
hand_2 = deal_hand(my_deck)
print(f"{hand_1}:\n\t{high_card(hand_1)}")
print(f"{hand_2}:\n\t{high_card(hand_2)}")
Here's the output:
% python3 Project4.py
[('5', 'diamonds'), ('7', 'spades'), ('2', 'clubs'), ('2', 'hearts'), ('J', 'clubs')]:
11
[('5', 'clubs'), ('5', 'hearts'), ('8', 'spades'), ('J', 'hearts'), ('A', 'spades')]:
14
And my code
def high_card(hand):
"""
The highest ranked card in your hand with an ace being
the highest and two being the lowest.
"""
sorted_hand = sort_hand(hand)
high_rank = get_rank(sorted_hand[-1])
points = RANKS.index(high_rank) + 2
return points
rate_hand
Now we've got every possible hand covered, we can write our core ranking algorithm. Try to do it by yourself. A sensible structure is a conditional dispatch.
The function should return an integer as the ranking for a hand. Each hand should have a ranking 1 point higher than the hand ranked below it.
Here's some test code:
my_deck = make_deck()
hand_1 = deal_hand(my_deck)
hand_2 = deal_hand(my_deck)
print(f"hand_1: {hand_1}\nhand_2: {hand_2}")
print(f"hand_1 points: {rate_hand(hand_1)}\nhand 2 points: {rate_hand(hand_2)}")
Here's the output:
% python3 Project4.py
hand_1: [('10', 'hearts'), ('9', 'spades'), ('Q', 'hearts'), ('9', 'clubs'), ('A', 'clubs')]
hand_2: [('3', 'clubs'), ('A', 'spades'), ('2', 'diamonds'), ('3', 'spades'), ('8', 'clubs')]
hand_1 points: 15
hand 2 points: 15
% python3 Project4.py
hand_1: [('4', 'clubs'), ('10', 'spades'), ('3', 'clubs'), ('6', 'spades'), ('Q', 'clubs')]
hand_2: [('4', 'spades'), ('10', 'hearts'), ('Q', 'spades'), ('3', 'spades'), ('J', 'clubs')]
hand_1 points: 12
hand 2 points: 12
% python3 Project4.py
hand_1: [('7', 'spades'), ('J', 'spades'), ('3', 'hearts'), ('8', 'hearts'), ('8', 'clubs')]
hand_2: [('Q', 'clubs'), ('6', 'diamonds'), ('K', 'clubs'), ('K', 'hearts'), ('Q', 'hearts')]
hand_1 points: 15
hand 2 points: 16
And here's a simple solution:
def rate_hand(hand):
if royal_flush(hand):
return 23
elif straight_flush(hand):
return 22
elif four_of_a_kind(hand):
return 21
elif full_house(hand):
return 20
elif flush(hand):
return 19
elif straight(hand):
return 18
elif three_of_a_kind(hand):
return 17
elif two_pair(hand):
return 16
elif one_pair(hand):
return 15
else:
return high_card(hand)
Partial refactoring
While it made for a good exercise, writing all the hand functions was repetitive. In practice, we want to avoid repeating ourselves. That's what programmers call the DRY principle. DRY stands for Don't Repeat Yourself. That's because it's easier for bugs to creep into your code when there's a lot of repetition. It gets harder to maintain, too.
Another thing that makes our code bad is some "smart" hacks. I put smart between quotes because using hacks in such basic code is everything but smart.
We want our program to be shorter, better, clearer.
The basic helper functions stay the same.
RANKS = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
SUITS = ["hearts", "diamonds", "clubs", "spades"]
def make_deck():
deck = set()
for rank in RANKS:
for suit in SUITS:
card = (rank, suit)
deck.add(card)
return deck
def deal_hand(deck):
hand = []
for i in range(0, 5):
card = deck.pop()
hand.append(card)
return hand
def get_rank(card):
return card[0]
def get_suit(card):
return card[1]
The first thing to realize is some hands extend other, more basic hands in some way. For example, royal flushes and straight flushes both extend flushes. We could rewrite them to call flush
internally.
flush
can become simpler, too. Instead of creating a list of our hand's suits with [get_suit(card) for card in hand]
, we'll use a set. We can define sets with set comprehensions. They work the same as list comprehensions. So we can translate suit_list
to suits = {get_suit(card) for card in hand}
. Since we don't have duplicates in sets, suits
will have only one element if the hand's a flush.
def flush(hand):
"""
Five cards of the same suit.
"""
suits = {get_suit(card) for card in hand}
return len(suits) == 1
Now on to royal flush
. Try refactoring the function by yourself before checking my solution. Use a set definition to create a set of the hand's ranks. Then check for equality between that set and possible ranks
. Also, use flush
inside the function.
def royal_flush(hand):
"""
10, jack, queen, king, ace, all of the same suit
"""
possible_ranks = {'10', 'J', 'Q', 'K', 'A'}
ranks = {get_rank(card) for card in hand}
return (possible_ranks == ranks) and flush(hand)
Look how simpler the function has become!
While writing straight
and straight_flush
, I realized most functions had a variable rank_list
. This means we're creating the same list several times to rank a single hand of cards. Creating the list once inside rate_hand
is better programming practice. We can then pass it as an argument to functions that need it. While we're at it, let's sort rank_list
.
def rate_hand(hand):
sorted_hand = sort_hand(hand)
rank_list = [get_rank(card) for card in sorted_hand]
if royal_flush(hand):
return 23
elif straight_flush(hand, rank_list):
return 22
elif four_of_a_kind(hand, rank_list):
return 21
elif full_house(hand, rank_list):
return 20
elif flush(hand):
return 19
elif straight(hand, rank_list):
return 18
elif three_of_a_kind(hand, rank_list):
return 17
elif two_pair(hand, rank_list):
return 16
elif one_pair(hand, rank_list):
return 15
else:
return high_card(sorted_hand)
Before I rewrite straight_flush
to call straight
and flush
, I'll rework straight
. Recall that I hated my code for straight
. It used a "smart" hack using string manipulation on list data. Our new straight will not be simpler or shorter than the old version but more robust.
I'll use the Sliding Window Technique. Try to work through my code by yourself, then read my explanations.
def contiguous_sublist(larger, smaller):
"""
Checks if smaller is a part of larger.
Inputs:
larger: list type
smaller: list type
Outputs:
bool type
"""
if not smaller or not larger:
return False # smaller or larger is an empty list
len_smaller = len(smaller)
len_larger = len(larger)
if len_smaller > len_larger:
return False # can't be a sublist if bigger than the list
for i in range(len_larger - len_smaller + 1):
window = larger[i:i+len_smaller]
if window == smaller:
return True
return False
def straight(hand, rank_list):
"""
Five cards of any suits, in sequential order.
"""
return contiguous_sublist(RANKS, rank_list)
We created a helper function called contiguous_sublist
. It checks whether smaller
is part of larger
. The first step is to check if either smaller
and larger
are empty lists. This will never happen, but I've learned not to trust myself too much. This step is
if not smaller or not larger:
return False
Then, we create two variables to hold the lengths of smaller
and larger
. This is not a necessary step, and you can skip it; this is a matter of personal preference.
Next, we check for the implausible case where smaller
is bigger than larger
.
if len_smaller > len_larger:
return False
At last is the meat of the function. This is the Sliding Window Technique. What we do is we "slide" element by element through larger
. At each step, we look at a "window" of larger
. We check if that window is equal to smaller
.
for i in range(len_larger - len_smaller + 1):
window = larger[i:i+len_smaller]
if window == smaller:
return True
return False
As an exercise, try to understand why I used len_larger - len_smaller + 1
and [i:i+len_smaller]
. Experiment with different values.
Now we can write a very simple straight_flush
.
def straight_flush(hand, rank_list):
"""
five cards of the same suit in sequential order.
"""
straight_status = straight(hand, rank_list)
flush_status = flush(hand)
return straight_status and flush_status
The next realization is that four_of_a_kind
, three_of_a_kind
, and one_pair
are similar. So we can write a general solution called n_of_a_kind
. Try to write it yourself. It should take a hand, a rank_list, and an integer n as arguments.
You can check your code against mine below:
def n_of_a_kind(hand, rank_list, n):
"""
Any n cards of the same rank
"""
for rank in RANKS:
if rank_list.count(rank) == n:
return True
return False
def four_of_a_kind(hand, rank_list):
"""
Any four cards of the same rank
"""
return n_of_a_kind(hand, rank_list, 4)
def three_of_a_kind(hand, rank_list):
"""
Any three cards of the same rank
"""
return n_of_a_kind(hand, rank_list, 3)
def one_pair(hand, rank_list):
"""
Any two numerically matching cards.
"""
return n_of_a_kind(hand, rank_list, 2)
full_house
and two_pairs
were unique enough that I kept them separate. I didn't forget to turn rank_list
from an internal variable to a function argument.
def full_house(hand, rank_list):
"""
Three of a kind and a pair
"""
pair = False
triple= False
for rank in RANKS:
if rank_list.count(rank) == 3:
triple = True
elif rank_list.count(rank) == 2:
pair = True
return pair and triple
def two_pair(hand, rank_list):
"""
Two different pairs in the same hand.
"""
pairs = 0
for rank in RANKS:
if rank_list.count(rank) == 2:
pairs += 1
return pairs == 2
high_card
also doesn't change much.
def high_card(hand):
"""
The highest ranked card in your hand with an ace being
the highest and two being the lowest.
"""
high_rank = get_rank(hand[-1])
points = RANKS.index(high_rank) + 2
return points
Creating the simplest possible poker game
Comparing hands
We have a hand ranker. But we want to play poker games. To have players and winners. We are not ther yet. I will write the most basic game mechanism possible. In fact, our game will barely be a poker game. There will be a variable number of players who all receive five cards from the deck. The program compares their hands, chooses a winner, and displays a message.
We will start with a function that takes several hands as arguments and returns a summary of the results. I added a copious amount of print statements to help you make sense of what each part of the code does.
def compare_hands(*hands):
"""
Compare a variable number of poker hands.
Returns a list containing dictionaries for each hand.
Each dictionary contains
- a player id
- the hand
- its rating
- its order (1 being the best).
"""
if len(hands) < 2:
print("Error: Compare more than two hands.")
return
print(len(hands))
ratings = [rate_hand(hand) for hand in hands]
print(f"ratings:\t{ratings}")
sorted_ratings = sorted(ratings, reverse=True)
print(f"sorted_ratings:\t{sorted_ratings}")
rankings = [sorted_ratings.index(rating) + 1 for rating in ratings]
print(f"rankings:\t{rankings}")
results = []
for i in range(0, len(hands)):
print(i)
dict = {
'player': i + 1,
'hand': hands[i],
'rating': ratings[i],
'ranking': rankings[i],
}
results.append(dict)
return results
The first interesting bit is *hands
. The asterisk '*
' means the function accepts a variable number of arguments stored inside a tuple. *
collects the arguments inside a tuple. When we define a function def f(*args):
, f
can have as many arguments as we want! Suppose f
takes integers as arguments, then we can call it like this f(1, 3, 6, 2, 3, 2)
.
In compare_hands
, we access the hands in a tuple. Suppose we have 3 players (and thus three hands to compare), hands would look like (<hand1>, <hand2>, <hand3>)
.
Since we accept variable numbers of arguments, we need to check if there are enough of them with
if len(hands) < 2:
print("Error: Compare more than two hands.")
return
We should also check if the number of players doesn't exceed 10. I leave it to you as an exercise.
Next, we create three lists, mirroring hands: ratings
, rankings
, and results
.
ratings
will hold the scores from applying rate hand
to each hand of hands
. We then sort these ratings in reverse order so we can create rankings
. While ratings
holds the raw scores (e.g., 20 for a full house), rankings
holds the rank of the hands when we sort them by score. The best hand would have a ranking of 1, the second best of 2, etc. It's possible to have hands with the same score. You can thus have duplicates in rankings (e.g., [1, 2, 3, 1]
.)
results
will hold useful info for each hand. A dictionary per hand is a natural choice, making the properties easy to access.
Here's how it looks when we run the code
>>> deck = make_deck()
>>> hand_1 = deal_hand(deck)
>>> hand_2 = deal_hand(deck)
>>> hand_3 = deal_hand(deck)
>>> hand_4 = deal_hand(deck)
>>> print(compare_hands(hand_1, hand_2, hand_3, hand_4))
4
ratings: [15, 14, 15, 15]
sorted_ratings: [15, 15, 15, 14]
rankings: [1, 4, 1, 1]
0
1
2
3
[{'player': 1,
'hand': [('5', 'hearts'), ('5', 'spades'), ('6', 'spades'), ('7', 'diamonds'), ('Q', 'hearts')],
'rating': 15,
'ranking': 1},
{'player': 2,
'hand': [('A', 'spades'), ('Q', 'diamonds'), ('3', 'clubs'), ('6', 'hearts'), ('5', 'diamonds')],
'rating': 14,
'ranking': 4},
{'player': 3,
'hand': [('A', 'hearts'), ('A', 'diamonds'), ('2', 'clubs'), ('6', 'diamonds'), ('10', 'spades')],
'rating': 15,
'ranking': 1},
{'player': 4,
'hand': [('4', 'clubs'), ('10', 'hearts'), ('10', 'diamonds'), ('9', 'clubs'), ('8', 'clubs')],
'rating': 15,
'ranking': 1}]
Reporting results
As I said, payers can get a tie. We will display different messages if there's a sole winner vs several winners.
First, I pick out the winners from compare_hands
's return value.
def detect_winners(results):
winners = [result for result in results if result['ranking'] == 1]
return winners
Note a feature of list comprehensions we haven't yet seen. The code after the if
filters the elements of results
. In this case, Python checks if a player's ranking is 1 (i.e., a winner) to include it in the winners
list.
Now we can print results to the screen:
def report_winner(results):
winners = detect_winners(results)
if len(winners) > 1:
print("TIE")
print(f"{len(winners)} players got tied!")
for i in range(0, len(winners)):
print(f"Player {winners[i]['player']} got a score of {winners[i]['rating']} with {winners[i]['hand']}")
else:
print(f"Player {winners[0]['player']} is the winner with a score of {winners[0]['rating']} with {winners[0]['hand']}")
But wait... Players don't care about their hand's rating. After all, it's an internal mechanism we created to compare hands. We can write a function with a conditional dispatch on the rating, returning a helpful string.
def get_hand_name(score):
if score < 15:
name = f"NO HAND... Highest card is a {RANKS[score - 2]}."
elif score == 15:
name = "ONE PAIR."
elif score == 16:
name = "TWO PAIRS."
elif score == 17:
name = "THREE OF A KIND."
elif score == 18:
name = "a STRAIGHT."
elif score == 19:
name = "a FLUSH."
elif score == 20:
name = "a FULL HOUSE."
elif name == 21:
name = "FOUR OF A KIND."
elif name == 22:
name = "a STRAIGHT FLUSH."
elif name == 23:
name = "a ROYAL FLUSH!!!"
else:
name = "what???"
return name
Now we can rewrite report_winner
:
def report_winner(results):
winners = detect_winners(results)
if len(winners) > 1:
print("TIE")
print(f"{len(winners)} players got tied!")
for i in range(0, len(winners)):
print(f"Player {winners[i]['player']} got {get_hand_name(winners[i]['rating'])} His hand is {winners[i]['hand']}")
else:
print(f"Player {winners[0]['player']} is the winner with {get_hand_name(winners[0]['rating'])} His hand is {winners[0]['hand']}")
# GAME
deck = make_deck()
hand_1 = deal_hand(deck)
hand_2 = deal_hand(deck)
hand_3 = deal_hand(deck)
hand_4 = deal_hand(deck)
results = compare_hands(hand_1, hand_2, hand_3, hand_4)
report_winner(results)
Let's play a game!
The results are:
% python3 Project4.py
TIE
2 players got tied!
Player 2 got ONE PAIR. His hand is [('3', 'diamonds'), ('4', 'clubs'), ('K', 'hearts'), ('K', 'spades'), ('A', 'diamonds')]
Player 3 got ONE PAIR. His hand is [('2', 'diamonds'), ('6', 'diamonds'), ('J', 'hearts'), ('J', 'spades'), ('4', 'hearts')]
% python3 Project4.py
Player 2 is the winner with TWO PAIRS. His hand is [('3', 'clubs'), ('6', 'clubs'), ('5', 'spades'), ('3', 'diamonds'), ('6', 'diamonds')]
% python3 Project4.py
Player 1 is the winner with a STRAIGHT. His hand is [('J', 'clubs'), ('7', 'hearts'), ('9', 'clubs'), ('10', 'clubs'), ('8', 'spades')]
Recap
Here are the main topics we covered today.
Sets
Sets are a collection type that is
unordered
unindexed
contains unique elements
contains immutable elements only
Note that while we can't access the elements by index, we can loop through a set.
We define sets with set()
if we want an empty set or with curly braces if we want to include elements: {1, 2, 3}
.
We add elements with the add()
method. The pop()
method removes and returns an arbitrary element from the set.
We can check if an object is an element of a set with in
and not in
. You can compare sets with ==
.
We'll see more set operations like union, intersection, etc, in future posts. They'll unlock the real power of sets.
Tuples
Tuples are also collections. They are immutable and ordered.
You can create tuples with parentheses.
Two important operations on tuples are packing and unpacking.
Packing is when you "pack" several values inside a tuple without using parentheses.
>>> my_tuple = 1, 2, 3
>>> my_tuple
(1, 2, 3)
When you give Python some comma-separated objects, it will create a tuple.
Unpacking is the inverse operation. You extract elements from a tuple into separate variables.
>>> a, b, c = my_tuple
>>> a
1
>>> b
2
>>> c
3
The number of variables on the left must match the number of elements in the tuple. Try to type a, b, c, d = my_tuple
. You'll get an error. You can do extended unpacking with the *
operator
>>> my_tuple = 1, 2, 3, 4, 5, 6
>>> a, *b, c = my_tuple
>>> a
1
>>> b
[2, 3, 4, 5]
>>> c
6
Python collects all but the first and the last element in a list when you use the *
operator.
Wait, what?
Remember variable function arguments? *
put them inside a tuple, not a list. This is a matter of context. Where we use *
matters.
Tuples are useful for data that won't change. They mesh well with sets because they're immutable.
List and set comprehensions
We saw a new way to define lists and sets.
The syntax for lists is
[expression for item in iterable if condition]
Set comprehensions work in a very similar way:
{expression for item in iterable if condition}
Comprehensions let us create new sets and lists with one-liners. They remove the boilerplate of for loops.
Here's how you create a list of integers between 0 and 100 inclusive.
numbers = [n for n in range(0, 101)]
Can you guess what this does?
[number**2 for number in numbers if number % 3 == 1]
This code is the same as
l = []
for number in numbers:
if number % 3 == 1:
l.append(number ** 2)
l
Isn't it much shorter?
By the way, the result of the last example is
[1, 16, 49, 100, 169, 256, 361, 484, 625, 784, 961, 1156, 1369, 1600, 1849, 2116, 2401, 2704, 3025, 3364, 3721, 4096, 4489, 4900, 5329, 5776, 6241, 6724, 7225, 7744, 8281, 8836, 9409, 10000]
Lambdas
We got our first brush with lambdas, one of my favorite programming constructs. They let you create anonymous functions. You can use these functions in whichever way you damn well please.
>>> (lambda x: x + 2)(3)
5
OK, my bad, that was terrible style. Don't do that.
Anonymous functions are best used as arguments to other functions.
The syntax is:
lambda arguments: expression
Functions as first-class objects
This ties into our last point. We can use functions in the same way we do other Python objects, like integers, lists, etc.
This might not seem like much. However, many programming languages lack that feature. Their syntax can become very cumbersome.
In the same way that we can pass functions like any other object, we can also return functions like any other object. So, we can create functions with functions as arguments that return functions.
Here's an example:
>>> def compose(f, g):
... return lambda x: g(f(x))
>>> def double(x):
... return x * 2
>>> def square(x):
... return x ** 2
>>> double_then_square = compose(double, square)
>>> square_then_double = compose(square, double)
>>> double_then_square(3)
36
>>> square_then_double(3)
18
What would have happened if we defined double_then_square
like this instead?
>>> def double_then_square():
... return compose(double, square)
Why do we need to type double_then_square()(3)
to get 36 as an answer?
Functions with a variable number of args
As we said earlier, the *
operator lets you create functions with as many or as few arguments as possible. Those arguments live inside a tuple.
The Sliding Window Technique
This is an algorithm (i.e., a 'recipe' for your computer). It checks if a sequence is a contiguous subsequence of a larger sequence. A contiguous subsequence is
Comprised in the larger sequence, i.e., all its members are part of the sequence.
Contiguous, meaning all in one piece. The elements should appear in the subsequence exactly like in the sequence.
Here's the algorithm.
Start at the beginning of the sequence
Create a 'window' the same size as your subsequence.
Check if this window is the same as your subsequence.
If it is, you got yourself a contiguous subsequence. Congrats!
If not, move one element further into the sequence.
Go back to step 2.
Exercises
Sets
Create a function that takes a list of numbers as arguments. It returns a set of all unique values in the list.
Write a function
f
taking a list of numbers as an argument. It should return a set of booleans. To construct the set, test if a list element is even. If it is even, addTrue
to the set, otherwise, addFalse
. Thus, lists containing both even and odd numbers should result in{True, False}
. Lists of even numbers would give{True}
. Lists of odd numbers should look like{False}
. The empty list should give you{}
.Write a program that takes a string and returns the set of unique characters in the string. Ignore case (i.e., lowercase vs caps) and spaces.
Write a function that accepts two sets of integers. It checks whether the first set is a subset of the second.
Tuples
Create a tuple with 5 elements. Use unpacking to assign the first three elements to separate variables. The remaining two elements should go in a list.
Write a function that takes a tuple of integers and returns a tuple of only the odd integers.
Let's do some geometry. We'll represent coordinates with tuples like
(1, 2)
,(3, -1)
, or(0, 0)
. Write a function that calculates the distance between two points. Remember Pythagoras!Now, let's do the Manhattan distance. The Manhattan distance is the total distance measured along grid lines. Like you would cruise along the perpendicular streets of New York. In math terms, we want the sum of the absolute difference of the coordinates.
Create a tuple of tuples. Each inner tuple contains a number and its square. Generate the tuples for numbers 1 to 10.
List comprehensions
Create a list of squares for numbers 1 through 10.
Write a function taking a list of strings. It returns a list of the strings longer than 3 characters.
Use a list comprehension to find the numbers between 1 and 100 divisible by 3 and 5.
Write a list comprehension to generate all pairs
(x, y)
so that x is a number between 1 and 5, and y is between 6 and 10.
Final project
Check out a particular poker ruleset. Implement it by reusing the functions we wrote in this post. You can add all the features you like (e.g., betting, face down vs face up cards, etc).
I hope you enjoyed this post. If you have any ideas, or any questions, I’d be glad to hear about them. Please leave a comment below the article!
Subscribe to my newsletter
Read articles from Had Will directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
