6
\$\begingroup\$

This is a program that would allow the users to evaluate themselves on a swimming training, it saves the evaluation in a new text file with the date as a title. There are many repeated blocks of code how can I make them smaller?

# We want to get the user input for the warm-up and the main-set.
# We want the user to evaluate himself in the warm-up and the main-set.
# We will allow the user to enter comments about specific sets.
# We want to save the users evaluations so that they can be read later.
# Add error exceptions when done.
import time


def commentw():
    global w_comments
    print("What are your comments for the warm-up?")
    comments = input()
    w_comments.append(comments)


def commentms():
    global ms_comments
    print("What are your comments for the main-set?")
    comments = input()
    ms_comments.append(comments)


warmup = []
while True:
    print("Warm-up:")
    print("Insert number of reps (10, 20, 50,...), type exit to move on to the evaluation.")
    reps = input()
    if reps.lower() == "exit" and warmup != []:
        break
    try:
        val = int(reps)
    except ValueError:
        print("Please enter the data properly")
        continue
    if int(reps) == 0:
        continue

    print("Insert distance(50, 100, 200,...), type exit to move on to evaluation.")
    distance = input()
    if distance.lower() == "exit":
        break
    try:
        val = int(distance)
    except ValueError:
        print("Please enter the data properly.")
        continue
    if int(distance) == 0:
        continue

    print("Insert type (Butterfly drills, Freestyle all-out,...), type exit to move on to the evaluation.")
    types = input()
    if types.lower() == "exit":
        break
    print("\n"*20)
    warmup.append(reps + " x " + distance + " " + types)


print("\n"*20)
print("Evaluation:")
# In the warm-up, the user needs to evaluate himself in each set of reps.
# In order to do that, he will have to evaluate his technique.
# This can be done by knowing how many reps were performed with an ideal technique and focus.
w_evaluation = []
w_total = 0
for item in warmup:
    print("How many reps were performed with intense focus on the technique in the " + item + "?")
    n = int(item.split()[0])
    answer = n+1
    while answer > n or answer == "":
        answer = float(input())
    w_evaluation.append(item + ": " + str(answer/n*10)+"/10.0")
    w_total += answer/n*10


print("\n"*20)
mainset = []
while True:
    print("Main-set:")
    print("Insert number of reps (10, 20, 50,...), type exit to move on to the evaluation.")
    reps = input()
    if reps.lower() == "exit" and mainset != []:
        break
    try:
        val = int(reps)
    except ValueError:
        print("Please enter the data properly.")
        continue

    print("Insert distance(50, 100, 200,...), type exit to move on to evaluation.")
    distance = input()
    if distance.lower() == "exit":
        break
    try:
        val = int(distance)
    except ValueError:
        print("Please enter the data properly.")
        continue

    print("Insert type (Butterfly drills, Freestyle all-out,...), type exit to move on to the evaluation.")
    types = input()
    if types.lower() == "exit":
        break
    print("\n"*20)
    mainset.append(reps + " x " + distance + " " + types)

print("\n"*20)
print("Evaluation:")
# For the main-set evaluation, the user will evaluate himself according technique, speed and focus.
ms_evaluation = []
focus_on = []
ms_total = 0
for item in mainset:
    print("How many reps in " + item + " were performed with the appropriate pace?")
    pace = float(input())
    print("How many reps in " + item+" were performed with appropriate technique?")
    tech = float(input())
    print("How many reps in " + item+" were performed with appropriate focus?")
    focus = float(input())
    n = int(item.split()[0])
    if pace/n < (n/2)/n:
        focus_on.append("Focus on pace in " + item)
    if tech/n < (n/2)/n:
        focus_on.append("Focus on your technique in " + item)
    if focus/n < (n/2)/n:
        focus_on.append("Improve your focus in " + item)
    ms_evaluation.append(item + ": " + str(((pace+tech+focus)/(n*3))*10)+"/10")
    ms_total += ((pace+tech+focus)/(n*3))*10

# Now it is time to print the evaluations for the user and allow him to write comments.
print("\n"*20)
print("============\nWarm-up evaluation\n============")
for item in w_evaluation:
    print(item)
print("Total: " + str(w_total))

print("\n"*20)
print("============\nMain-set evaluation\n============")
for item in ms_evaluation:
    print(item)
print("Total: " + str(ms_total))

print("Would you like to add comments on certain parts?")
w_comments = []
ms_comments = []
print("These comments could include what to focus on during the next practice.")
print("Type exit to save and exit")
answer = ""
while answer != "exit":
    print("use (w) for warm-up and (m) for main-set.")
    answer = input()
    if answer == "w":
        commentw()
    if answer == "m":
        commentms()

# Now it is time to save everything
# We want to save the evaluation, and the comments
name = time.strftime("%d.%m.%y.txt")
f = open(name, "w+")
f.write("============\nWarm-up evaluation\n============\n")
for item in w_evaluation:
    f.write(item+"\n")
f.write("Total: " + str(w_total))
f.write("\n\nComments: \n")
for item in w_comments:
    f.write(item+"\n")

f.write("\n============\nMain-set evaluation\n============\n")
for item in ms_evaluation:
    f.write(item+"\n")
f.write("Total: " + str(ms_total))
f.write("\n\nComments: \n")
for item in ms_comments:
    f.write(item+"\n")
for item in focus_on:
    f.write(item+"\n")
f.close()
\$\endgroup\$
0

2 Answers 2

7
\$\begingroup\$

functions

Instead of 1 large script, that gets done line per line, split it into logical functions, this allows for code-reuse.

Use 1 main-method, that you call behind a if __name__ == '__main__':-guard

special conditions

If you have a function asking for input where you want 'exit' to break the flow, use an Exception, that's what they are meant for.

class Exit(Exception): pass    

def ask_positive_int(name: str) -> int:
    while True:
        reps = input(
            f"Insert number of {name}, type exit to move on to the evaluation.")
        if reps.lower() == 'exit':
            raise Exit
        try:
            reps = int(reps)
            if reps >= 0:
                return reps
        except ValueError:
            pass
        print('Answer a correct number')

return instead of print

this way you can reuse the answers in more then one place, or if you change the user interface, you can do so without too much trouble

generators

instead of instantiating an empty list, appending to it and finally returning this list, yield the elements instead:

def ask_excercises():
    while True:
        try:
            reps = ask_reps()
            distance = ask_distance()
            excercise_type = ask_type()
        except Exit:
            return
        yield reps, distance, excercise_type

open file with a with-statement

To make sure the file is close, use context managers

hoist the IO

put the IO at a level as high as possible, as mentioned in this talk by Brandon Rhodes.

Saving the file, together with the previous points becomes something like this:

filename = time.strftime("%d.%m.%y.txt")
with open(filename, 'w+') as filehandle:
    save_evaluation(
        filehandle,
        warmup_evaluation,
        main_evaluation,
        comments
    )

multi-line strings

instead of

print("Would you like to add comments on certain parts?")
print("These comments could include what to focus on during the next practice.")
print("Type exit to save and exit")

you can do:

print("""
    Would you like to add comments on certain parts?
    These comments could include what to focus on during the next practice.
    Type exit to save and exit
    """)

full code

from collections import defaultdict

import time


class Exit(Exception): pass


def ask_reps() -> int:
    """
    asks the number of repititions to the

    retries untill the user enters a valid integer >= 0 or `exit`
    Returns
    -------
    int
        the number of repititions or `None` when the user enters `exit`
    """
    return ask_positive_int('reps')


def ask_distance() -> int:
    return ask_positive_int('distance')


def ask_positive_int(name: str) -> int:
    while True:
        reps = input(
            f"Insert number of {name}, type exit to move on to the evaluation.")
        if reps.lower() == 'exit':
            raise Exit
        try:
            reps = int(reps)
            if reps >= 0:
                return reps
        except ValueError:
            pass
        print('Answer a correct number')


def ask_evaluation(excercise, characteristic):
    while True:
        answer = input(
            f'How many reps in {excercise}  were performed with the appropriate {characteristic}?')
        try:
            return float(answer)
        except ValueError:
            print('answer a correct number')


def ask_type():
    answer = input(
        f'Insert type of excercise, type exit to move on to the evaluation.')
    if answer.lower() == 'exit':
        raise Exit
    return answer


def clear_screen():
    print('\n' * 20)


def ask_excercises():
    while True:
        try:
            reps = ask_reps()
            distance = ask_distance()
            excercise_type = ask_type()
        except Exit:
            return
        yield reps, distance, excercise_type


def ask_warmup_evaluation(excercises):
    for excercise in excercises:
        score = ask_evaluation(
            excercise_string(excercise),
            'intense focus on the technique'
        )
        yield excercise, score


def excercise_string(excercise):
    reps, distance, excercise_type = excercise
    return f'{reps} x {distance} {excercise_type}'


def ask_main_evaluation(excercises):
    focus_on = []
    evaluation = []
    for excercise in excercises:
        reps, distance, excercise_type = excercise
        characteristics = ['pace', 'technique', 'focus']
        scores = []
        for characteristic in characteristics:
            answer = ask_evaluation(excercise, characteristic)
            if answer / reps < 0.5:
                focus_on.append((excercise, characteristic))
            scores.append(answer)
        total = sum(scores) / len(scores)
        evaluation.append((excercise, total))
    return evaluation, focus_on,


def print_evaluation(evaluation, filehandle=None):
    total = 0
    for excercise, score in evaluation:
        print(f'{excercise_string(excercise)}: {score} / 10', file=filehandle)
        total += score

    print(f'Total: {total}', file=filehandle)


def get_comments():
    print("""
    Would you like to add comments on certain parts?
    These comments could include what to focus on during the next practice.
    Type exit to save and exit
    """)
    comments = defaultdict(list)
    comment_types = {
        'w': 'warm-up',
        'm': 'main-set',
    }
    message = '\n'.join(
        f'use ({key}) for {description}.'
        for key, description in comment_types.items()
    )
    while True:

        comment_type = input(message).lower()
        if comment_type == 'exit':
            return comments
        comment_type = comment_types.get(comment_type, None)
        if comment_type is None:
            print('enter a valid part to comment on')
            continue
        msg = f'What are your comments for the {comment_types[comment_type]}'
        comment = input(msg)
        comments[comment_type].append(comment)

    return comments


def save_evaluation(
        filehandle,
        warmup_evaluation,
        main_evaluation,
        comments,
        focus_on
):
    filehandle.write("""
    ==================
    Warm-up evaluation
    ==================
    """)
    print_evaluation(warmup_evaluation, filehandle=filehandle)

    filehandle.write("""
    ===================
    Main-set evaluation
    ===================
    """)
    print_evaluation(main_evaluation, filehandle=filehandle)

    filehandle.write("""
        ========
        Comments
        ========""")
    for comment_type, comment_strings in comments.items():
        filehandle.write(comment_type)
        filehandle.writelines(comment_strings)

    filehandle.write("""
    ========
    Focus on
    ========""")
    filehandle.writelines(focus_on)


def main():
    print('Warm-up:')
    warmup_excercises = ask_excercises()
    clear_screen()
    print("Evaluation:")
    warmup_evaluation = list(ask_warmup_evaluation(warmup_excercises))
    print_evaluation(warmup_evaluation)
    clear_screen()

    print("Main-set:")
    main_excercises = ask_excercises()
    clear_screen()
    print("Evaluation:")
    main_evaluation, focus_on = ask_main_evaluation(main_excercises)
    print_evaluation(main_evaluation)
    clear_screen()

    comments = get_comments()
    clear_screen()

    filename = time.strftime("%d.%m.%y.txt")
    with open(filename, 'w+') as filehandle:
        save_evaluation(
            filehandle,
            warmup_evaluation,
            main_evaluation,
            comments,
            focus_on
        )


if __name__ == "__main__":
    main()
\$\endgroup\$
6
  • \$\begingroup\$ It returns an error TypeError: excercise_string() takes 1 positional argument but 3 were given \$\endgroup\$
    – Omar.B
    Jun 5, 2018 at 14:30
  • \$\begingroup\$ evaluation.append(excercise, total) TypeError: append() takes exactly one argument (2 given) would it be better to concatenate them instead? \$\endgroup\$
    – Omar.B
    Jun 5, 2018 at 15:53
  • \$\begingroup\$ it should be a tuple (excercise, total), as you can see later in the code where it's used or in the other evaluation method \$\endgroup\$ Jun 5, 2018 at 16:05
  • \$\begingroup\$ to find bugs like that and other omissions, you should/can write unit-tests \$\endgroup\$ Jun 5, 2018 at 16:06
  • \$\begingroup\$ It should also be done here: focus_on.append((excercise, characteristic)). Also one more thing, I don't think we need this line comment_type = comment_types.get(comment_type, None) since we are looking for the key here not the value. Otherwise, it will return a key error. Thanks for helping :) \$\endgroup\$
    – Omar.B
    Jun 5, 2018 at 16:24
2
\$\begingroup\$

Honestly, it looks like only three blocks of code were re-usable, so I made them into functions. I made a comments function to get the users comments, an enter_exercise function to input the exercise type, and a print_eval function to print the evaluation. All take 2 main parameters, the first is the type ('warm-up' or 'main-set') that will just be used for output and the second is the list to which the input will be appended (or just output in the case of print_eval, which also takes a total value for output).

There were a few other code changes for simplicity and clarity.

  1. There is no need to do a print('something') before a value = input(), changed those to value = input('something')
  2. Everytime you cast an input you should surround it with a try...except ValueError and then surround that in a loop so if they enter something invalid you'll give them another opportunity to input a value.

import time

def get_num(msg,ex_list):
    while True:
        num = input(msg)
        if num.lower() == "exit" and ex_list != []:
            return -1
        try:
            num = int(num)
        except ValueError:
            print("Please enter the data properly")
            continue

        if num <= 0:
            print("Please enter number greater than 0")
            continue
        else:
            return num

def get_float(msg):
    while True:
        try:
            return float(input(msg))
        except ValueError:
            print("Please enter a number.")
            continue

def comments(comment_type,comment_list):
    comments = input("What are your comments for the {}?".format(comment_type))
    comment_list.append(comments)
    return comment_list

def enter_exercise(ex_type,ex_list):
    while True:
        print("{}:".format(ex_type))
        reps = get_num("Insert number of reps (10, 20, 50,...), type exit to move on to the evaluation.",ex_list)
        if reps==-1:
            return ex_list

        distance = get_int("Insert distance(50, 100, 200,...), type exit to move on to evaluation.",ex_list)
        if distance==-1:
            return ex_list

        types = input("Insert type (Butterfly drills, Freestyle all-out,...), type exit to move on to the evaluation.")
        if types.lower() == "exit" and ex_list != []:
            return ex_list
        print("\n"*20)
        ex_list.append(reps + " x " + distance + " " + types)
    return ex_list

def print_eval(ev_type,ev_list,ev_total):
    print("\n"*20)
    print("============\n{} evaluation\n============".format(ev_type))
    for item in ev_list:
        print(item)
    print("Total: " + str(ev_total))

warmup = []
warmup = enter_exercise('Warm-up',warmup)

print("\n"*20)
print("Evaluation:")
# In the warm-up, the user needs to evaluate himself in each set of reps.
# In order to do that, he will have to evaluate his technique.
# This can be done by knowing how many reps were performed with an ideal technique and focus.
w_evaluation = []
w_total = 0
w_evaluation,w_total = eval_exercise(warmup,w_total,w_evaluation)
for item in warmup:
    print("How many reps were performed with intense focus on the technique in the " + item + "?")
    n = int(item.split()[0])
    answer = n+1
    while answer > n or answer == "":
        try:
            answer = float(input())
        except ValueError:
            print("Please enter a number less than or equal to {}.".format(str(n)))
    w_evaluation.append(item + ": " + str(answer/n*10)+"/10.0")
    w_total += answer/n*10

print("\n"*20)
mainset = []
mainset = enter_exercise('Main-set',mainset)

print("\n"*20)
print("Evaluation:")
# For the main-set evaluation, the user will evaluate himself according technique, speed and focus.
ms_evaluation = []
focus_on = []
ms_total = 0
for item in mainset:
    pace = get_float("How many reps in " + item + " were performed with the appropriate pace?")
    tech = get_float("How many reps in " + item+" were performed with appropriate technique?")
    focus = get_float("How many reps in " + item+" were performed with appropriate focus?")

    n = int(item.split()[0])
    if pace/n < (n/2)/n:
        focus_on.append("Focus on pace in " + item)
    if tech/n < (n/2)/n:
        focus_on.append("Focus on your technique in " + item)
    if focus/n < (n/2)/n:
        focus_on.append("Improve your focus in " + item)
    ms_evaluation.append(item + ": " + str(((pace+tech+focus)/(n*3))*10)+"/10")
    ms_total += ((pace+tech+focus)/(n*3))*10

# Now it is time to print the evaluations for the user and allow him to write comments.
print_eval('Warm-up',w_evaluation,w_total)

print_eval('Main-set',ms_evaluation,ms_total)

print("Would you like to add comments on certain parts?")
w_comments = []
ms_comments = []
print("These comments could include what to focus on during the next practice.")
print("Type exit to save and exit")
answer = ""
while answer != "exit":
    answer = input("use (w) for warm-up and (m) for main-set.").lower()
    if answer == "w":
        w_comments = comments('warm-up',w_comments)
    elif answer == "m":
        ms_comments = comments('main-set',ms_comments)
    elif answer == 'exit':
        break

# Now it is time to save everything
# We want to save the evaluation, and the comments
# 
# Did not change anything beyond this point
\$\endgroup\$
2
  • \$\begingroup\$ a lot of the while True blocks are still repeated, with only a few words in a string different, so they can be refactored out too. You can extract at least a get_int(message) and get_float(message) method \$\endgroup\$ Jun 4, 2018 at 13:13
  • \$\begingroup\$ putting code in different functions is not only to prevent code repetition, but also to make the general control flow more clear, as a kind of code documentation and to be able to change the program later in an easier fashion \$\endgroup\$ Jun 4, 2018 at 13:19

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.