4
\$\begingroup\$

What improvements can I make? Should I organize my functions based on A-Z? For example, find_letter function comes before guessing function.

import json
import random
import re


def find_letter(user_input, vocab, i=None):
    return [i.start() for i in re.finditer(user_input, vocab)]


def guessing(tries,vocab,blank_line):
    while tries > 0:
        user_input = input("Guess:")

        if user_input in vocab:
            if not(user_input in blank_line):
                index = find_letter(user_input=user_input,vocab=vocab)
                i = 0
                while i < len(index):
                    blank_line[index[i]] = user_input
                    i += 1

                if not('_' in blank_line):
                    return "you won!!!"
            else:
                print("YOU ALREADY TRIED THAT LETTER!")
        else:
            tries -= 1

        print(str(blank_line) + "  Number of Tries : " + str(tries))

    return "you lost"


def hangman_game(vocab):
    'Preliminary Conditions'
    print(introductory(vocab=vocab))
    tries = num_of_tries(vocab=vocab)
    blank_line = produce_blank_lines(vocab=vocab)
    print(blank_line)

    print(guessing(tries=tries,vocab=vocab,blank_line=blank_line))


def introductory(vocab):
    return "HANGMAN GAME. So...what you gotta do is guess and infer what the word might be. " \
           "WORD COUNT : " + str(len(vocab))


def num_of_tries(vocab):
    if len(vocab) < 7:
        return 8
    elif 7 <= len(vocab) < 10:
        return 10
    else:
        return 14


def produce_blank_lines(vocab):
    blank_line = []
    for i in vocab:
        if i.isalpha():
            blank_line += "_"
        elif i == " ":
            blank_line += " "
        else:
            blank_line += i + " "

    return blank_line

if __name__ == '__main__':
    data = json.load(open("vocabulary.json"))
    vocab = random.choice(list(data.keys())).lower()
    print(vocab)
    print(hangman_game(vocab=vocab))

Snippet of the JSON File

{"abandoned industrial site": ["Site that cannot be used for any purpose, being contaminated by pollutants."], "abandoned vehicle": ["A vehicle that has been discarded in the environment, urban or otherwise, often found wrecked, destroyed, damaged or with a major component part stolen or missing."], "abiotic factor": ["Physical, chemical and other non-living environmental factor."], "access road": ["Any street or narrow stretch of paved surface that leads to a specific destination, such as a main highway."], "access to the sea": ["The ability to bring goods to and from a port that is able to harbor sea faring vessels."], "accident": ["An unexpected, unfortunate mishap, failure or loss with the potential for harming human life, property or the environment.", "An event that happens suddenly or by chance without an apparent cause."], "accumulator": ["A rechargeable device for storing electrical energy in the form of chemical energy, consisting of one or more separate secondary cells.\\n(Source: CED)"], "acidification": ["Addition of an acid to a solution until the pH falls below 7."], "acidity": ["The state of being acid that is of being capable of transferring a hydrogen ion in solution."], "acidity degree": ["The amount of acid present in a solution, often expressed in terms of pH."], "acid rain": ["Rain having a pH less than 5.6."]
\$\endgroup\$
7
  • 5
    \$\begingroup\$ @ close-voter: Answers on this site are always "opinion-based." I don't even know why that's a close-vote reason. Anyway, this question is fine. Also to answer the OP's one explicit question: Almost certainly you should not arrange these six functions in a-z order. :) \$\endgroup\$ Commented Dec 26, 2018 at 23:54
  • 3
    \$\begingroup\$ Please include an example (even tiny no problem) json file for easier testing. (The question is fine upvote from me) \$\endgroup\$
    – Caridorc
    Commented Dec 27, 2018 at 0:17
  • \$\begingroup\$ @Quuxplusone How do programmers organize functions then? \$\endgroup\$
    – austingae
    Commented Dec 27, 2018 at 2:32
  • 2
    \$\begingroup\$ @austingae How do programmers organize functions then? Typically by topic, and fine-grained enough that the order doesn't matter. For example you might put everything related to Hangman in hangman.py; or once that gets too big, you'd put everything related to the user interface in hangman-ui.py and everything related to core gameplay in hangman-core.py; or once that gets too big, you'd split out hangman-dictionary.py into its own file... And then sure, I'd probably sort those files alphabetically in my manifest or Makefile or whatever. \$\endgroup\$ Commented Dec 27, 2018 at 2:43
  • 2
    \$\begingroup\$ It occurs to me that sorting functions by name has the dictionary problem: I can't find the function (say, introductory()) unless I already know how to spell it! And if I know how to spell it (say, my IDE has a sidebar index of all the functions in the current file) then I likely don't care where in the file it is (since I can jump there using my IDE). But everyone has an "IDE" for the filesystem (ls), and the filenames themselves make a rough "index by topic" — dictionary problem solved! \$\endgroup\$ Commented Dec 27, 2018 at 2:50

1 Answer 1

3
\$\begingroup\$
data = json.load(open("vocabulary.json"))

Unless Python 3 has changed this, I think you're leaking the open file handle here. What you want is more like

with open("vocabulary.json") as f:
    data = json.load(f)

which IMHO is even a bit easier to read because it breaks up the mass of nested parentheses.

vocab = random.choice(list(data.keys())).lower()

Same deal with the nested parentheses here. We can save one pair by eliminating the unnecessary materialization into a list. [EDIT: Graipher points out that my suggestion works only in Python 2. Oops!]

Also, vocab (short for vocabulary, a set of words) isn't really what this variable represents. data.keys() is our vocabulary; what we're computing on this particular line is a single vocabulary word. So:

word = random.choice(data.keys()).lower()
print(word)
print(hangman_game(word))

I can tell you're using Python 3 because of the ugly parentheses on these prints. ;) But what's this? Function hangman_game doesn't contain any return statements! So it basically returns None. Why would we want to print None to the screen? We shouldn't be printing the result of hangman_game; we should just call it and discard the None result.

word = random.choice(data.keys()).lower()
print(word)
hangman_game(word)

def hangman_game(vocab):

Same comment about vocab versus word here.

'Preliminary Conditions'

This looks like you maybe forgot a print? Or maybe you're trying to make a docstring? But single quotes don't make a docstring AFAIK, and even if they did, "Preliminary Conditions" is not a useful comment. I'd just delete this useless line.

print(introductory(vocab=vocab))

In all of these function calls, you don't need to write the argument's name twice. It's a very common convention that if you're calling a function with only one argument, the argument you pass it is precisely the argument it needs. ;) Also, even if the function takes multiple parameters, the reader will generally expect that you pass it the right number of arguments in the right order. You don't have to keyword-argument-ize each one of them unless you're doing something tricky that needs calling out. So:

print(introductory(word))

And then you can inline introductory since it's only ever used in this one place and it's a one-liner already:

print(
    "HANGMAN GAME. So...what you gotta do is guess and infer what the word might be. "
    "WORD COUNT : " + str(len(vocab))
)

Personally, I would avoid stringifying and string-concatenation in Python, and write simply

print(
    "HANGMAN GAME. So...what you gotta do is guess and infer what the word might be. "
    "WORD COUNT : %d" % len(vocab)
)

I know plenty of working programmers who would write

print(
    "HANGMAN GAME. So...what you gotta do is guess and infer what the word might be. "
    "WORD COUNT : {}".format(len(vocab))
)

instead. (I find that version needlessly verbose, but it's definitely a popular alternative.)

tries = num_of_tries(vocab=vocab)
blank_line = produce_blank_lines(vocab=vocab)
print(blank_line)
print(guessing(tries=tries,vocab=vocab,blank_line=blank_line))

It's odd that you abbreviated number_of_tries to num_..., but then left in the word of. I would expect either number_of_tries or num_tries (or ntries for the C programmers in the audience); num_of_tries is in a weird no-man's-land.

It's weird that you have a function named produce_blank_lines that produces actually a single blank_line. It's even weirder that the value of blank_line is not a constant "\n"! This suggests that everything here is misnamed. You probably meant underscores = produce_underscores(word) or even blanks = shape_of(word).

Finally, it is weird that blank_line is not even a string, but rather a list of characters.

I would replace this entire dance with something like

blanks = ''.join('_' if c.isalpha() else ch for ch in word)

or

blanks = re.sub('[A-Za-z]', '_', word)

This would lose your addition of a blank space after each non-space-non-alpha character, but IMHO that's actually an improvement over your version. (Your sample JSON doesn't show any examples of words containing non-space-non-alpha characters. My version certainly performs better for words like non-space or Route 66.)


if not(user_input in blank_line):

This would be idiomatically expressed (both in Python and in English) as

if user_input not in blank_line:

index = find_letter(user_input=user_input,vocab=vocab)
i = 0
while i < len(index):
    blank_line[index[i]] = user_input
    i += 1

You have another singular/plural problem here (just like with blank_line[s]). It doesn't make sense to get the len of a singular index. Since the variable actually represents a list of indices, we should name it [list_of_]indices.

Also, this style of for-loop is very un-Pythonic. In Python we can say simply

for i in find_letter(user_input, word):
    blank_line[i] = user_input

And then we can inline the single-use find_letter:

for m in re.finditer(user_input, word):
    blank_line[m.start()] = user_input

(Using m as the conventional name for a match object.) And then we can eliminate the regex and just loop over the word directly:

for i, ch in enumerate(word):
    if ch == user_input:
        blank_line[i] = user_input

or

for i in range(len(word)):
    if word[i] == user_input:
        blank_line[i] = user_input

In fact, by not using regexes, we fix a bug in your code: if the chosen vocabulary word contains . as one of its non-space-non-alpha characters, a user who enters . as their guess will win the game immediately! :D

Anyway, that's enough for one answer, I think.

\$\endgroup\$
6
  • 2
    \$\begingroup\$ Unfortunately your second recommendation produces the error TypeError: 'dict_keys' object does not support indexing when trying to randomly choose a key, so the wrapping with list is needed. In Python 2 this worked since the keys were a list already. \$\endgroup\$
    – Graipher
    Commented Dec 27, 2018 at 10:13
  • \$\begingroup\$ But you can get rid of the .keys(), random.choice(list(data)) works. \$\endgroup\$
    – Graipher
    Commented Dec 27, 2018 at 10:15
  • 2
    \$\begingroup\$ And nowadays f"WORD COUNT : {len(vocab)}" is all the rage, which is at least shorter again :) \$\endgroup\$
    – Graipher
    Commented Dec 27, 2018 at 10:17
  • 2
    \$\begingroup\$ Ah, I mistakenly thought random.choice could handle any iterable. If OP had written random.choice(list(data)) I probably would have complained that that approach was too "cute" and not expressive enough. ;) The problem of a random.choice that works on arbitrary iterables is discussed here: stackoverflow.com/questions/12581437/… \$\endgroup\$ Commented Dec 27, 2018 at 16:09
  • \$\begingroup\$ If I have many variables to concatenate in a string, isn't it better to use +. But if I only have a variable to concatenate at the end of the string, then is it better to use %s or %d? \$\endgroup\$
    – austingae
    Commented Dec 28, 2018 at 18:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.