Lisping in Python
This is more or less what some students will write to evaluate whether the AI player they just wrote is any good:
def win_rate(player, opponents=["random_player", "minimax_player(1)", "minimax_player(2)"], N=10): """Return the win rate for the given player against a specifiable roster of opponents, averaged over N trials.""" answer = {} for opponent in opponents: score = [] for _ in range(N//2): g = game(player, eval(opponent)) score += [player1_wins(g)] for _ in range(N//2): g = game(eval(opponent), player) score += [player2_wins(g)] answer[opponent] = np.mean(score) return answer
How would you review this piece of code ?
As far a code quality goes, this is not bad. The names are all well chosen and meaningful, the docstring is accurate, the implementation is straightforward, and avoid a tricky pitfall: the "first player bias", in which the first player has a statistically significant advantage.
One could nitpick the reliance on eval()
, which is a bit hacky, but within the
wider context makes perfect sense11: trust me. , and the reliance on
playerX_wins
returning a bool
which in numerical context map to 1
and 0
,
allowing np.mean
to compute the win rate. This is implicit but not terribly
unexpected.
The worst I can see is that when N
is odd and >=3
, the number of trials is
actually N-1
. This does not matter for a big enough N
.
Would you rewrite this function ? If so, how ?
Ever since I've learned Lisp, I tend to write in Python as I would in Lisp, making use of list and dictionary comprehension:
def win_rate(player, opponents=["random_player", "minimax_player(1)", "minimax_player(2)"], N=10): """Return the win rate for the given player against a specifiable roster of opponents, averaged over N trials.""" return {opponent: np.mean( [player1_wins(game(player, eval(opponent))) for _ in range(N//2)] + [player2_wins(game(eval(opponent), player)) for _ in range(N//2)]) for opponent in opponents}
I find this formulation better than the first:
- for starters, it is shorter (5 lines v.s. 11, i.e. half as much code),
- it removes the
answer
,g
, andscore
local variables, which each take up one slot in the reader's working memory, - the judicious use of white space allow one to immediately grok the difference between the two half loops.
Yet, my students struggle to understand the latter form more than they struggle understanding the first one.
Maybe it is because they lack fundamental mathematical training. The lispish
form is closer to the what would be the mathematical formalization of the
win_rate
function: "Let win_rate
be a function that returns a mapping from
each opponent to the win rate of the given player against said opponent".
Maybe it is because Lisp is a write-once language, and the elegance of the second implementation is only visible to the author.
In any case, Python is flexible enough to accomodate both styles, and that makes it a nice teaching language despite its horribly irregular syntax.
1. Changelog
Initial version