Available doors
Why is this a stand-alone function? Why are door identifiers hard-coded here? The MontyHallBox
knows what the doors are, and what they are called:
class MontyHallBox(object):
def available_doors(self):
return list(self.__doors.keys())
Class Syntax
class MontyHallBox(object):
...
is obsolete syntax. Use:
class MontyHallBox:
...
Class Private Names
Why use a double underscore for self.__doors
and self.__init_doors()
? This is a Python feature for avoiding name collision in derived classes, and "mangles" the names is a predictable fashion (adding the classname as a prefix):
>>> mhb = MontyHallBox()
>>> mhb._MontyHallBox__doors
{1: 'car', 2: 'goat', 3: 'goat'}
>>>
It does not provide any extra security of the member.
Use set notation
Instead of set([initial_door, noncar_door])
, write {initial_door, noncar_door}
.
Throw-away variables
The variable i
is never used:
for i in range(num):
results.append(simn_fun(mhb))
The PEP-8 recommendation is to use _
for these throw-away variables:
for _ in range(num):
results.append(simn_fun(mhb))
List comprehension
Building up a list of results should be done using list comprehension, instead of initialization & repeated .append
operations. Instead of:
results = []
for i in range(num):
results.append(simn_fun(mhb))
Use:
results = [simn_fun(mhb) for _ in range(num)]
Counting
There is no need to build up a long list of results, just to count the result distributions. Pass the results as they are generated to a counter.
>>> import collections
>>> num = 10000
>>> simn_fun = run_sim_always_switch
>>> counts = collections.Counter(simn_fun(mhb) for _ in range(num))
>>> counts['success']
6717
>>> counts['fail']
3283
or, knowing True
is 1 and False
is zero, count the successes as you generate them yourself:
>>> success = sum(simn_fun(mhb) == 'success' for _ in range(num))
>>> success
6614
Formatting
Use f-strings to put the variables/expressions directly in the format statements:
print(f'Always switch, run simulation {num} times.')
print(f'Success ratio: {run_simn(num, run_sim_always_switch)}')
print(f'Never switch, run simulation {num} times.')
print(f'Success ratio: {run_simn(num, run_sim_never_switch)}')
Round results to the desired significant figures with appropriate format codes:
print(f'Success ratio: {run_simn(num, run_sim_never_switch):.3f}')
Named Constants
If you accidentally wrote the tests as if item == 'Car':
, you'd find that nobody wins, and would have to hunt down where in the code you made spelling/capitalization errors. The IDE won't help you; it can't auto complete strings for you. If you used named constants ...
CAR = 'car'
GOAT = 'goat'
... and you wrote if item == Car:
, instead of nobody winning, the program would crash with an unknown identifier and identity the line where the problem exists, which is much better than having to search through all of the code looking for where logic is going wrong. But the above named "constants" aren't actually constants; they are variables we hope never get accidentally (or maliciously) changed.
from enum import Enum
Prize = Enum('Prize', 'CAR, GOAT')
Now we have Prize.CAR
and Prize.GOAT
, which are constants.
Malicious Strategies
A friend can say they've come up with a great strategy - and let you test their code:
import friend
run_simn(10_000, friend.strategy)
And lo and behold, they always win the car! How is this possible?
Friend strategy 1:
def strategy(mhb):
initial_door = next(door for door, prize in mhb._MontyHallBox__doors.items()
if prize == 'car')
_ = mhb.reveal_noncar(initial_door)
item = mhb.reveal_selected(initial_door)
return 'success' if item == 'car' else 'fail'
Friend strategy 2:
def strategy(mhb):
initial_door = 1
goats = { mhb.reveal_noncar(initial_door) for _ in range(10) }
if len(goats) == 2:
selected_door = initial_door
else:
selected_door = 2 if goats == { 3 } else 3
item = mhb.reveal_selected(initial_door)
return 'success' if item == 'car' else 'fail'
Friend strategy 3:
def strategy(mhb):
return 'success'
They are cheating. Some friend.
A safer (harder to cheat) simulation approach
Let's start by creating some type aliases, to make clearer code.
from typing import Callable, Counter, List, NewType, Tuple
# Doors are numbered
Door = NewType('Door', int)
Doors = List[Door]
This Door
is just a fancy way of saying the int
type-hint, and Doors
is a fancy way of saying the List[int]
type-hint.
Now let's create two methods which the let's the user play the Monty Hall game:
def first_pick(doors: Doors) -> Door:
"""
Ask the player to pick a door
"""
return int(input(f"Pick a door [{', '.join(map(str,doors))}]: "))
def second_pick(doors: Doors, selected: int, goats: Doors) -> Door:
"""
Show some goats behind doors the player didn't pick.
Ask them again to pick a door.
"""
print(f"Monty shows you a goat behind door #{', '.join(map(str, goats))}.")
choice = input(f"Pick a new door [default: {selected}]: ")
return int(choice) if choice else selected
We pass all the information the player needs to make their choices. In the first case, just the list of available Doors
. In the second, we add what their original Door
selection was, and the Doors
Monty Hall revealed goats behind. No extra MontyHallBox
object, or self
parameter that the user can peek into to cheat and get extra information.
We can even add types for these two functions up where we are defining the types we're using:
FirstPick = Callable[[Doors], Door]
SecondPick = Callable[[Doors, Door, Doors], Door]
Now, let's build the actual game:
def monty_hall(first: FirstPick = first_pick, second: SecondPick = second_pick, *,
cars: int = 1, goats: int = 2, reveal: int = 1) -> Tuple[Door, Prize]:
"""
Play a Monty Hall type game.
The player picks one of several doors, behind each is either a Car or a Goat.
Monty Hall shows the player behind one of the doors they didn't pick,
"Oh look, behind this door is a Goat! Now, do you want to change your mind?"
The player now gets alter their selection, but does it pay to change their mind?
Parameters:
first: given the doors, pick a door
second: given the doors, the first pick, and what was revealed, pick a door
cars: number of cars (default 1)
goats: number of goats (default 2)
reveal: number of hidden goats to reveal (default 1)
Returns:
The door they choose, and the Prize behind it (a Goat or a Car)
"""
# Sanity checks
if cars < 1 or goats < 1:
raise ValueError("Rigged game! There must be both cars & goats!")
if reveal >= goats:
raise ValueError("Monty is not allowed to reveal all of the goats!")
# Set up the game
prizes = [Prize.CAR] * cars + [Prize.GOAT] * goats
random.shuffle(prizes)
doors = list(range(1, len(prizes) + 1))
game = { door: prize for door, prize in zip(doors, prizes) }
# Ask player to make their initial selection...
selected = first(doors)
# Monty reveals some doors
goats = [ door for door, prize in game.items()
if prize == Prize.GOAT and door != selected ]
random.shuffle(goats)
goats = goats[:reveal]
# Based on original choice and revealed doors, ask player makes new choice...
selected = second(doors, selected, goats)
# ... and give the player their prize
return selected, game[selected]
Ok, I've beefed up the game a little bit. We've got a configurable number of cars (default 1), goats (default 2), and doors to reveal (default 1). We've got first
and second
functions which default to the first_pick
and second_pick
user player functions above. We've got a long doc-string describing the function. Then we've got some validation on number of cars, goats, and reveals.
Then we have the meat-and-potatoes of the function. Create the prizes, mix them up, and hide them behind doors. Call the first method to get the initial door selection. Then Monty Hall picks some doors with goats to reveal. The second the second function is called to get the final door selection. Finally, the resulting prize is returned.
Want to play the game?
def human_verses_monty(**kwargs):
door, prize = monty_hall(**kwargs)
print(f"Behind door #{door}, you find a {prize.name}!")
Use human_verses_monty()
. Or maybe human_verses_monty(cars=3, goats=3, reveal=2)
I'm using **kwargs
for brevity, so I can pass additional keywords through to the underlying monty_hall(...)
method. It would be clearer to list all of the keyword arguments out, but this post is already getting pretty long, and I'm not done yet.
Playing multiple times
Now that we have our safe simulation, let's make it so we can play the game multiple times, and collect the results:
def monty_halls(strategy: SecondPick, games, *,
initial: FirstPick = random.choice, **kwargs) -> Counter[Prize]:
return Counter(monty_hall(initial, strategy, **kwargs)[1] for _ in range(games))
The initial door pick defaults to random.choice
. We need to pass in a strategy
for the second pick. The game is played the given number of times, with Door, Prize
being returned, from which we select only the Prize
with [1]
, and count the number of times we get each prize in a Counter
.
Now we need some strategies:
def always_switch(doors: Doors, selected: Door, goats: Doors) -> Door:
return random.choice(list(set(doors) - set(goats) - {selected}))
def never_switch(doors: Doors, selected: int, goats: Doors) -> Door:
return selected
Again, no extra information to allow them to cheat. And they don't get to break the rules and play incorrectly (asking Monty Hall multiple times to reveal doors) to cheat.
How about function to evaluate various strategies:
def evaluate(strategies: List[SecondPick], games, **kwargs):
for strategy in strategies:
prizes = monty_halls(strategy, games, **kwargs)
wins = prizes[Prize.CAR]
losses = prizes[Prize.GOAT]
total = wins + losses
print(f"{strategy.__name__}: wins {wins / total * 100:.2f} %")
print()
Let's see it in action:
if __name__ == '__main__':
print("Standard game (1 car, 2 goats)")
evaluate([always_switch, never_switch], 10_000)
print("Alternate game (2 car, 3 goats, 2 reveals)")
evaluate([always_switch, never_switch], 10_000, cars=2, goats=3, reveal=2)
Running this gives:
Standard game (1 car, 2 goats)
always_switch: wins 66.88 %
never_switch: wins 33.12 %
Alternate game (2 car, 3 goats, 2 reveals)
always_switch: wins 80.14 %
never_switch: wins 39.91 %
Cheaters will be cheaters
It is still possible to cheat. One method would be to capture the stack frame, and look at the local variables in the calling frame. We can get around this by using a separate thread or process to execute the strategy calls in, so the caller can't simply walk up the stack looking for gold. This still isn't fool proof, 'cause the cheater could look for the parent thread, or parent process and try to inspect those.
At least we aren't making it easy to cheat, like the original MontyHallBox
approach did.