Skip to content

ይህ ትምህርት ገና ወደ አማርኛ አልተተረጎመም፤ ስለዚህ በእንግሊዝኛ ቀርቧል። የእንግሊዝኛውን ገጽ ክፈቱ

Project: Refactor with functions

6 min readፕሮጀክት

In lesson 2.6 you built a grade calculator as one long block. Now you'll rebuild it from small functions, each with one job, and see what that buys you: code that checks itself, and pieces you can reuse in the guessing game from lesson 3.5 without copying.

Here's the finished grade calculator, with a mistyped score first:

Output
Score (0-100): 101
Please enter a number from 0 to 100.
Score (0-100): 85
Grade: A (4.0 points)

The whole program is four functions, built one milestone at a time. Keep these names; later milestones reuse them:

FunctionIts one job
grade_for(score)Return the letter for a score. Never prints.
grade_points(letter)Return the points for a letter. Never prints.
ask_number(prompt, low, high)Ask until the answer is a whole number in range; return it.
main()Run the program: ask, grade, print.

Milestone 1: grade_for, and a test

Start with the grading rule from lesson 2.6, as a function that returns the letter. Because it returns, a few lines can check it at every boundary: each pair below is a score and the letter it should get. The check prints nothing for a right answer and a clear line for a wrong one.

መልመጃ

grade_for passes every check

Write grade_for(score) using lesson 2.6's table: 85 and up is A, 70 to 84 is B, 50 to 69 is C, below 50 is F. The starter version returns F for everything, so the checks report every score that isn't an F. Make the checks pass.

Your code

def grade_for(score):
    # Return "A", "B", "C", or "F"
    return "F"

checks = [(0, "F"), (49, "F"), (50, "C"), (69, "C"), (70, "B"), (84, "B"), (85, "A"), (100, "A")]
for score, expected in checks:
    if grade_for(score) != expected:
        print(f"Wrong for {score}: got {grade_for(score)}, expected {expected}")
print("All checks done")

In the editor, press Escape then Tab to move on.

መፍትሄውን አሳይ

This is one way to solve it, not the only one. If yours prints the same thing, it works.

def grade_for(score):
    if score >= 85:
        return "A"
    elif score >= 70:
        return "B"
    elif score >= 50:
        return "C"
    else:
        return "F"

checks = [(0, "F"), (49, "F"), (50, "C"), (69, "C"), (70, "B"), (84, "B"), (85, "A"), (100, "A")]
for score, expected in checks:
    if grade_for(score) != expected:
        print(f"Wrong for {score}: got {grade_for(score)}, expected {expected}")
print("All checks done")

Now break it on purpose: change >= 85 to > 85 and run it. The checks catch it straight away:

def grade_for(score):
    if score > 85:
        return "A"
    elif score >= 70:
        return "B"
    elif score >= 50:
        return "C"
    else:
        return "F"

checks = [(0, "F"), (49, "F"), (50, "C"), (69, "C"), (70, "B"), (84, "B"), (85, "A"), (100, "A")]
for score, expected in checks:
    if grade_for(score) != expected:
        print(f"Wrong for {score}: got {grade_for(score)}, expected {expected}")
print("All checks done")

In the editor, press Escape then Tab to move on.

This is why grade_for returns instead of printing. Predict what a printing version gives the check:

ውጤቱን ገምቱ

Decide before you look. Guessing wrong is how this sticks.

def grade_for(score):
    if score >= 85:
        print("A")
    else:
        print("F")

print(grade_for(90) == "A")
Pick the output

Python prints

A
False

The call prints A for a person to read, then returns None. None == "A" is False, so a check would call a correct grade wrong.

A printed answer has to be checked by a person reading it. A returned value can be compared with != by the program itself: in lesson 2.6 you typed ten scores in by hand; now eight checks run every time you change the code. Module 9 builds on exactly this.

Milestone 2: ask_number

The calculator needs a score from 0 to 100; the guessing game needs a guess from 1 to 100. One function can do both if the range is passed in. text.isdigit() (lesson 3.5) checks the text is all digits before int() converts it, so ninety can't crash it.

መልመጃ

Ask until the answer is valid

Write ask_number(prompt, low, high): ask with input(prompt) until the answer is all digits and from low to high, then return it as an int. For a bad answer, print Please enter a number from 0 to 100. (with the real low and high). The Input box has a word, a score that's too high, then 85.

Your code

def ask_number(prompt, low, high):
    # Ask until the text is all digits and in range, then return it as an int
    return int(input(prompt))

score = ask_number("Score (0-100): ", 0, 100)
print(score)

In the editor, press Escape then Tab to move on.

One answer per line, given to input() in order.

መፍትሄውን አሳይ

This is one way to solve it, not the only one. If yours prints the same thing, it works.

def ask_number(prompt, low, high):
    while True:
        text = input(prompt)
        if text.isdigit() and low <= int(text) <= high:
            return int(text)
        print(f"Please enter a number from {low} to {high}.")

score = ask_number("Score (0-100): ", 0, 100)
print(score)

ጥያቄ

ask_number loops with while True and has no break. What ends the loop?

isdigit() comes first in the and, so int(text) only runs on digits (lesson 2.3). Lesson 7.2 rewrites ask_number the better way, trying int() and handling the error, so it accepts numbers like -3 and asks again, instead of stopping, if someone types ፩.

Milestone 3: main()

grade_points uses a dict (lesson 4.4). main() is the only function that asks or prints; the others just calculate. You'll see if __name__ == "__main__": at the bottom of most Python programs; for now, read it as "if this file is the one being run". Lesson 8.5 shows why it matters.

መልመጃ

The whole calculator

Add grade_points(letter) and main() to your grade_for and ask_number, and print the result as Grade: A (4.0 points). The Input box has 101, then 85.

Your code

# Paste grade_for and ask_number here

def grade_points(letter):
    # Return 4.0, 3.0, 2.0, or 0.0
    pass

def main():
    # Ask for the score, grade it, and print the result
    pass

if __name__ == "__main__":
    main()

In the editor, press Escape then Tab to move on.

One answer per line, given to input() in order.

መፍትሄውን አሳይ

This is one way to solve it, not the only one. If yours prints the same thing, it works.

def grade_for(score):
    if score >= 85:
        return "A"
    elif score >= 70:
        return "B"
    elif score >= 50:
        return "C"
    else:
        return "F"

def grade_points(letter):
    points = {"A": 4.0, "B": 3.0, "C": 2.0, "F": 0.0}
    return points[letter]

def ask_number(prompt, low, high):
    while True:
        text = input(prompt)
        if text.isdigit() and low <= int(text) <= high:
            return int(text)
        print(f"Please enter a number from {low} to {high}.")

def main():
    score = ask_number("Score (0-100): ", 0, 100)
    grade = grade_for(score)
    print(f"Grade: {grade} ({grade_points(grade)} points)")

if __name__ == "__main__":
    main()

When you move code into functions, keep the call at the bottom. Python runs the file from the top, and a def has to run before its function can be called:

Read main() aloud: ask for a number, get its grade, print it. Each line hides the details in a function with a clear name, and each function can be checked on its own.

Milestone 4: reuse ask_number

Lesson 3.5 needed a whole milestone to skip bad guesses. With ask_number, it's one line.

መልመጃ

The guessing game, with ask_number

Replace the input line with a call to ask_number for a guess from 1 to 100. The Input box includes a typo, abc, which shouldn't count as an attempt.

Your code

import random

def ask_number(prompt, low, high):
    while True:
        text = input(prompt)
        if text.isdigit() and low <= int(text) <= high:
            return int(text)
        print(f"Please enter a number from {low} to {high}.")

def main():
    random.seed(2019)
    secret = random.randint(1, 100)
    attempts = 0
    while True:
        guess = int(input("Guess: "))
        attempts = attempts + 1
        if guess > secret:
            print("Too high")
        elif guess < secret:
            print("Too low")
        else:
            print(f"You got it in {attempts} attempts!")
            return

if __name__ == "__main__":
    main()

In the editor, press Escape then Tab to move on.

One answer per line, given to input() in order.

መፍትሄውን አሳይ

This is one way to solve it, not the only one. If yours prints the same thing, it works.

import random

def ask_number(prompt, low, high):
    while True:
        text = input(prompt)
        if text.isdigit() and low <= int(text) <= high:
            return int(text)
        print(f"Please enter a number from {low} to {high}.")

def main():
    random.seed(2019)
    secret = random.randint(1, 100)
    attempts = 0
    while True:
        guess = ask_number("Guess: ", 1, 100)
        attempts = attempts + 1
        if guess > secret:
            print("Too high")
        elif guess < secret:
            print("Too low")
        else:
            print(f"You got it in {attempts} attempts!")
            return

if __name__ == "__main__":
    main()

main() never sees abc, so it never counts it. Delete the seed line to play for real.

Milestone 5: the Ethiopian calendar (stretch)

From lesson 2.2: 12 months of 30 days, then Pagume with 5 days, or 6 in a leap year, when year % 4 == 3. Two small functions, and the same kind of checks as milestone 1.

መልመጃ

is_leap and days_in_month

Write is_leap(year) returning True or False, and days_in_month(month, year) that uses it. Make the checks pass.

Your code

def is_leap(year):
    pass

def days_in_month(month, year):
    pass

checks = [(1, 2019, 30), (12, 2020, 30), (13, 2019, 6), (13, 2020, 5), (13, 2023, 6)]
for month, year, expected in checks:
    if days_in_month(month, year) != expected:
        print(f"Wrong for month {month} of {year}: got {days_in_month(month, year)}, expected {expected}")
print("All checks done")

In the editor, press Escape then Tab to move on.

መፍትሄውን አሳይ

This is one way to solve it, not the only one. If yours prints the same thing, it works.

def is_leap(year):
    return year % 4 == 3

def days_in_month(month, year):
    if month <= 12:
        return 30
    if is_leap(year):
        return 6
    return 5

checks = [(1, 2019, 30), (12, 2020, 30), (13, 2019, 6), (13, 2020, 5), (13, 2023, 6)]
for month, year, expected in checks:
    if days_in_month(month, year) != expected:
        print(f"Wrong for month {month} of {year}: got {days_in_month(month, year)}, expected {expected}")
print("All checks done")

year % 4 == 3 is already True or False, so is_leap returns it with no if.

ዋና ዋና ነጥቦች

  • Give each function one job, and a name that says it: grade_for, ask_number, main.
  • Functions that return are easy to test: compare the result with what it should be. Functions that print aren't.
  • Keep asking and printing in main(); keep the rules in functions that only calculate.
  • A function that takes its limits as parameters, like ask_number(prompt, low, high), can be reused in other programs.
  • if __name__ == "__main__": main() runs the program when the file is run.