Here are our solutions for the day 23 exercises in the 30 Days of Python series. Make sure you try the exercises yourself before checking out the solutions!

### 1) Write a generator that generates prime numbers in a specified range.

I'm going to be using this solution from the day 8 exercise as a starting point, since the logic is going to be almost entirely the same:

```
for dividend in range(2, 101):
for divisor in range(2, dividend):
if dividend % divisor == 0:
break
else:
print(dividend)
```

If you're not sure about why this works, I'd recommend looking at the original exercise walkthough.

Building on our earlier solution, the first thing we need to to create a function and place all of our code inside of it.

```
def gen_primes(limit):
for dividend in range(2, limit + 1):
for divisor in range(2, dividend):
if dividend % divisor == 0:
break
else:
print(dividend)
```

Here I've added a parameter called `limit`

which is going determine where our generator ultimately stops. It determines the highest number we're going to check.

With that, we're almost done. Now we just need to turn our function into a generator by using the `yield`

keyword. We're going to put this in the `else`

branch of the inner `for`

loop, instead of printing the `dividend`

.

```
def gen_primes(limit):
for dividend in range(2, limit + 1):
for divisor in range(2, dividend):
if dividend % divisor == 0:
break
else:
yield dividend
```

Now when we call our function we get back a generator iterator:

```
def gen_primes(limit):
for dividend in range(2, limit + 1):
for divisor in range(2, dividend):
if dividend % divisor == 0:
break
else:
yield dividend
primes = gen_primes(101) # <generator object gen_primes at 0x7f02ca556c80>
```

And we can use this generator iterator to retrieve primes one at a time using the `next`

function:

```
...
primes = gen_primes(101) # <generator object gen_primes at 0x7f02ca556c80>
print(next(primes)) # 2
print(next(primes)) # 3
print(next(primes)) # 5
```

### 2) Below we have an example where `map`

is being used to process names in a list. Rewrite this code using a generator expression.

```
names = [" rick", " MORTY ", "beth ", "Summer", "jerRy "]
names = map(lambda name: name.strip().title(), names)
```

The generator expression syntax is just like the comprehensions we've been writing before. The only difference is that we use regular round parentheses rather than square brackets or curly braces.

```
names = [" rick", " MORTY ", "beth ", "Summer", "jerRy "]
names = (name.strip().split() for name in names)
```

For cases like this, where we're forced to use a lambda expression, generator expressions generally end up being shorter and more readable.

## 3) Write a small program to deal cards for a game of Texas Hold'em.

The deal order is slightly complicated, but there is a detailed explanation in the day 23 post, so check back there if you need a reminder.

First things first, we need to create our deck of cards. There are several ways we can do this.

One of option you may have considered it something like this:

```
ranks = (2, 3, 4, 5, 6, 7, 8, 9, 10, "jack", "queen", "king", "ace")
suits = ("clubs", "diamonds", "hearts", "spades")
cards = []
for rank in ranks:
for suit in suits:
cards.append((rank, suit))
```

We can verify that all of the cards are there by printing `cards`

and printing `len(deck)`

, which should be `52`

for a standard 52 card deck of cards.

Since we're creating a new list from existing collections, it's also possible to do this with comprehensions, like this:

```
ranks = (2, 3, 4, 5, 6, 7, 8, 9, 10, "jack", "queen", "king", "ace")
suits = ("clubs", "diamonds", "hearts", "spades")
cards = [(rank, suit) for suit in suits for rank in ranks]
```

We can verify once again that we get what we want by printing the contents of `cards`

and the length of the collection.

However, instead of writing this comprehension, we can call upon the `itertools.product`

, which does very much the same thing, and it gives us back an iterator.

```
import itertools
ranks = (2, 3, 4, 5, 6, 7, 8, 9, 10, "jack", "queen", "king", "ace")
suits = ("clubs", "diamonds", "hearts", "spades")
cards = list(itertools.product(ranks, suits))
```

Any of these options are perfectly valid.

Now we should make a function to shuffle our deck and return a new iterator so that we can draw cards from it. To shuffle the deck we're going to use the `shuffle`

function in the `random`

module.

```
import itertools
import random
def shuffle_deck(cards):
deck = list(cards)
random.shuffle(deck)
return iter(deck)
ranks = (2, 3, 4, 5, 6, 7, 8, 9, 10, "jack", "queen", "king", "ace")
suits = ("clubs", "diamonds", "hearts", "spades")
cards = list(itertools.product(ranks, suits))
```

There are a few things involved in this `shuffle_deck`

function.

We accept a set of `cards`

as a parameter, which we used to compose a `deck`

. This `deck`

is a list for good reason: the `random.shuffle`

function takes in a sequence, and is going to perform an in-place shuffle. This means we need a mutable sequence type to work with.

We then shuffle the `deck`

by passing it to the `random.shuffle`

function. Once this is done return an iterator for the shuffled `deck`

which we created by passing the `deck`

to the `iter`

function.

Next we need to tackle a bit of user interaction, since we need to know how many players the user wants to play with.

I'm actually going to define a function in this case, because I want to handle any invalid input, and I want to reprompt the user if they provide invalid input. I also want to put in a check to make sure the number of players is within the range specified in the exercise (2 - 10).

```
def get_players():
while True:
number_of_players = input("How many players are there? ").strip()
try:
number_of_players = int(number_of_players)
except ValueError:
print("You must enter an integer.")
else:
if number_of_players in range(2, 11):
return number_of_players
elif number_of_players < 2:
print("You must have at least 2 players.")
else:
print("You can have a maximum of 10 players.")
```

Give that a try and make sure everything works.

Assuming there are no problems, it's time to make our functions to deal the cards. I think it makes sense to have a function to act as the dealer, which will shuffle the cards, and delegate to specialised functions dealing to the players, and dealing cards to the table.

Having a dealer function means we can easily play multiple hands.

Let's define all our functions and build them as we go:

```
import itertools
import random
def deal(cards, number_of_players):
deck = shuffle_deck(cards)
deal_to_players(deck, number_of_players)
deal_to_table(deck)
def deal_to_players(deck, number_of_players):
pass
def deal_to_table(deck)
pass
def get_players():
while True:
number_of_players = input("How many players are there? ").strip()
try:
number_of_players = int(number_of_players)
except ValueError:
print("You must enter an integer.")
else:
if number_of_players in range(2, 11):
return number_of_players
elif number_of_players < 2:
print("You must have at least 2 players.")
else:
print("You can have a maximum of 10 players.")
def shuffle_deck(cards):
deck = list(cards)
random.shuffle(deck)
return iter(deck)
ranks = (2, 3, 4, 5, 6, 7, 8, 9, 10, "jack", "queen", "king", "ace")
suits = ("clubs", "diamonds", "hearts", "spades")
cards = list(itertools.product(ranks, suits))
```

I've filled out the `deal`

function already, since it's fairly simple. It just takes in the cards our decks are composed of, and the number of players playing in this round.

In the function body, we construct our shuffled deck from the defined cards, and then we call the other deal functions to do all the hard work.

Let's turn to `deal_to_players`

. First things first, we have to figure out how to distribute the cards so that each player gets one card in turn, and each player ends up with two cards.

For this, I'm going to use a pair of list comprehensions like so:

```
def deal_to_players(deck, number_of_players):
first_cards = [next(deck) for _ in range(number_of_players)]
second_cards = [next(deck) for _ in range(number_of_players)]
```

It's important that we use a list comprehension here and *not* a generator expression, because we actually want these cards to be drawn *now*. You'll see why this is important in a moment.

Once we have our first and second cards in this lists, we can zip them together to create a hand for each player.

```
def deal_to_players(deck, number_of_players):
first_cards = [next(deck) for _ in range(number_of_players)]
second_cards = [next(deck) for _ in range(number_of_players)]
hands = zip(first_cards, second_cards)
```

This `zip`

step makes it very important that we don't have lazy collections assigned to `first_cards`

and `second_cards`

. It's not that `zip`

can't handle them—it can—but we have to think about what happens when we use this `zip`

object.

When we ask for an item from`zip`

, what does it do?

Well, first it asks for a card from `first_cards`

, the it asks for a card from `second_cards`

, it bundles them up into a tuple and it gives it to us. But `first_cards`

and `second_cards`

are both just asking for a card from `deck`

to give `zip`

the values it wants, and they're both drawing from the same deck iterator.

This means that each play will be dealt two cards in a row, rather than each player being dealt one card, and then another after each player has been dealt a card. This is a subtle point, but it's important that we be careful about things like this when working with lazy types.

Luckily, using a list comprehension solves this issue, because the cards are requested from the `deck`

when we're creating `first_cards`

, and again when we request `second_cards`

. By time we get to the `zip`

step, we already have the cards; we're just waiting to bundle them up.

Once we have the hands for the players, we just need to print them.

```
def deal_to_players(deck, number_of_players):
first_cards = [next(deck) for _ in range(number_of_players)]
second_cards = [next(deck) for _ in range(number_of_players)]
hands = zip(first_cards, second_cards)
print()
for i, (first_card, second_card) in enumerate(hands, start=1):
print(f"Player {i} was dealt: {first_card}, {second_card}")
print()
```

Now let's deal with the final part of the app, which is dealing the cards to the table. This is comparatively simple, because we don't need to worry about the number of players, or anything like that.

First, we need to burn a card, and then deal three cards to the table in one go:

```
def deal_to_table(deck):
next(deck) # burn
flop = ', '.join(str(next(deck)) for _ in range(3))
print(f"The flop: {flop}")
```

One thing to watch out for here is that `join`

requires an iterable containing strings. We therefore have to convert the tuple we get back from `next`

to a string using `str`

.

From here, it's just a case of burning and dealing a single card for the next two steps of the deal process.

```
def deal_to_table(deck):
next(deck) # burn
flop = ', '.join(str(next(deck)) for _ in range(3))
print(f"The flop: {flop}")
next(deck) # burn
print(f"The turn: {next(deck)}")
next(deck) # burn
print(f"The river: {next(deck)}")
print()
```

With that, we just need to call `deal`

and make sure that everything runs. Make sure your can call your `deal`

function multiple times!

```
import itertools
import random
def deal(cards, number_of_players):
deck = shuffle_deck(cards)
deal_to_players(deck, number_of_players)
deal_to_table(deck)
def deal_to_players(deck, number_of_players):
first_cards = [next(deck) for _ in range(number_of_players)]
second_cards = [next(deck) for _ in range(number_of_players)]
hands = zip(first_cards, second_cards)
print()
for i, (first_card, second_card) in enumerate(hands, start=1):
print(f"Player {i} was dealt: {first_card}, {second_card}")
print()
def deal_to_table(deck):
next(deck) # burn
flop = ', '.join(str(next(deck)) for _ in range(3))
print(f"The flop: {flop}")
next(deck) # burn
print(f"The turn: {next(deck)}")
next(deck) # burn
print(f"The river: {next(deck)}")
print()
def get_players():
while True:
number_of_players = input("How many players are there? ").strip()
try:
number_of_players = int(number_of_players)
except ValueError:
print("You must enter an integer.")
else:
if number_of_players in range(2, 11):
return number_of_players
elif number_of_players < 2:
print("You must have at least 2 players.")
else:
print("You can have a maximum of 10 players.")
def shuffle_deck(cards):
deck = list(cards)
random.shuffle(deck)
return iter(deck)
ranks = (2, 3, 4, 5, 6, 7, 8, 9, 10, "jack", "queen", "king", "ace")
suits = ("clubs", "diamonds", "hearts", "spades")
cards = list(itertools.product(ranks, suits))
deal(cards, get_players())
```

This was a long exercise, with some tricky components, but I hope you were able to manage. Feel free to ask any questions on our Discord server if you get stuck.