A starter note, throughout my answer: FP = "functional programming".
Possible Improvements
Use == rather than is for value comparisons.
I see if var is number in multiple places. Specifically, lines 29, 96, 109. One could argue that is reads better than ==, but the two are computationally different. (is breaks for large numbers.) Use == instead. See also: Is there a difference between “==” and “is”?
Thinking Functionally
FP tends to avoid mutability, i.e. changing state. FP is more about telling the computer what things are rather than what things do. Here are a couple lines of code from get_words (lines 9-12):
# filter the words so that they have the same number of characters as the word in play.
words = [word.lower() for word in words_temp if len(word) is word_len]
# Get rid of any possible duplicates in the file.
words = list(set(words))
Sweet, looks innocent. But you're assigning to words twice... you're changing its state.
I like how Miran Lipovača (author of a Haskell tutorial) puts it:
[Y]ou set variable
ato 5 and then do some stuff and then set it to something else. [...] If you say thatais 5, you can't say it's something else later because you just said it was 5. What are you, some kind of liar?
(source)
We can actually trim your two lines down to one by directly using a set comprehension and thereby eliminating the mutation of words (note also the replacement of is with ==):
words = list({word.lower() for word in words_temp if len(word) == word_len})
You could even return the list directly from the function!
Next, another interesting snippet (lines 29-33):
if len(guesses) is 0:
substitute = '.'
else:
# exclude all of the wrong guesses
substitute = f"[^{guesses}]"
This looks innocent too! But it also resembles an imperative statement: "if this, substitute is this, else substitute is that". We can make this more functional by clearly defining what substitute is:
substitute = '.' if len(guesses) == 0 else f"[^{guesses}]"
And this reads "substitute is '.' if this else that". (Note how the statement is now declarative and the variable becomes the subject of the statement.)
Yet another snippet (lines 113-117):
# Get the frequencies of each character in the possible words.
stats = get_statistics(possible_words)
# Remove characters we've already guessed from the statistics.
[stats.pop(guessed_letter, None) for guessed_letter in guesses]
Line 117 is a list comprehension, which is in itself functional... but it's changing the state of stats! Instead of removing the unneeded letters, make a new dictionary with the needed letters.
And back to my point: with functional programming, avoid mutability, define variables as what they are and not what they do/how they come about.
Game Loop and Mutability
The game loop... ah. It's a while-loop... and this presents a couple problems.
- While-loops tend to be imperative construct (telling the interpreter to loop while something isn't true).
- Since this is a dynamic game and since it's a single while-loop, you'll inevitably modify the state of surrounding variables to either keep track of progress.
Lipovača: "But you just said## Line 81 ## # the number of guesses the computer has made. num_of_guesses = 0 ## Line 127 ## # add our guess to the total listing of guesses. num_of_guesses += 1num_of_guessesis 0!"
Solution? Recursion. Pass in the mutable variables as arguments to the function and recurse all the way to the end. (Or of course, you could stick with the more readable while-loop. Some things are inevitable – sigh.)
Consider using type-hints.
This is really helpful in the world of FP. What does a function receive? What does it return? This allows you to reason with the input and output of functions. See also: What are Type hints in Python 3.5
Comments
Yes, there are quite a lot. Some are unnecessary... and some of them are untruths.
Lines 20-21:
# Load the words in from the words.txt file.
words = get_words(num_of_characters)
What if the implementation of get_words changes? This comment becomes obsolete.
Lines 47-48:
# Print the list of possible words.
return possible_words
No printing here. You print it sometime later in the game loop, but not here. Here, there's only a return, which in itself doesn't do any printing.
Instead, consider commenting what each function does, preferably using Python doc-strings.
def get_words(word_len):
"""
Returns a list of words each with length equal to `word_len`.
"""
As above, you can choose to leave out the details of the implementation. Sure, get_words will open, read, and close a file, but this won't have any side-effects1. Perhaps in the future, you might want to load the words from a database, and the doc-string won't need to be updated, because the input and output are unchanged.
1 – Unless if say, you're in a multi-threaded environment where the files will be accessed from different threads concurrently.
We also don't need the comment on line 20: # Load the words in from the words.txt file.. We can simply scroll to get_words and read the doc-string to know what it does.
See also: Self-Documenting Code; What is self-documenting code and can it replace well-documented code?
The Bright Side
Your program still has merits:
Use of Functions
Although all your functions are used only once, the functions clearly separate individual tasks, and this aids the reader to reason about the code.
Variables
Some are slightly redundant, but the names you've given them are helpful enough to remove at least a third of the comments.
Use of f-strings
f-strings are relatively new in Python, and they're not only more convenient, but also more functional over the OOP-variants: str.format and the %-printf notation.
Use of Comprehensions
I'm seeing quite a lot of comprehensions and no for-loop blocks. This is a merit: using a for-loop block with colon and suite bears the air of imperative programming (tells the interpreter to loop over an iterable), but comprehensions are more functional as they pack your values into a handy list/set/dictionary/generator expression.
PEP 8
Formatting is superb overall. What with snake_case, spacing, double line-breaks before and after functions, and a if __name__ == '__main__'. All this is good practice.
Keep it up!