5
\$\begingroup\$

I'm a beginner coder, doing it solely for fun, having started coding about two months ago with Python. I have a working piece of code for calculating area and volume of different geometric shapes, using user input of "radius".

As I have said, the code works but I would like to know how I can improve it to be shorter, faster and more efficient. Essentially, I think it would be instructive to compare my rank amateur code to the code that an experienced software developer would produce to accomplish the same result.

from tkinter import *
from tkinter import ttk
import sys

root = Tk()

pi = 3.141

# Option Box

var = StringVar(root)
var.set("Functions")

lbl_title = Label(root, text = "GEOMETRIC SHAPES AND VOLUMES")
lbl_title.grid(row = 0, column = 0, columnspan = 2, padx =5, pady = 10)

lbl_choose = Label(root, text = "Choose a function and hit OK:")
lbl_choose.grid(row = 1, column = 0, padx = 5, pady = 0, sticky = W)

box = ttk.OptionMenu(root, var, "Circle Circumference", "Circle Area", "Sphere Volume",
               "Rectangle Area", "Rectangle Prism Volume", "Cone Volume")
box.grid(row = 2, column = 0, padx=5, pady=5, sticky= W)

# Separator

line1 = ttk.Separator(root, orient = HORIZONTAL)
line1.grid(row = 4, column = 0, columnspan = 3, sticky = EW) # Note separator sticky EW!

#txt_data_in = Text(root, height = 1, width = 30, bg = "Light Grey")
#txt_data_in.grid(row = 5, column = 0, padx = 5, sticky = W)

# Functions

def circ_circumf(r):
    print("\nThe circumference of the circle is: " + str(2*pi*r) + " units.")

def circ_area(r):
    print("\nThe area of the circle is: " + str(pi*r**2) + " units.")

def sphere_vol(r):
    print("\nThe volume of the sphere is: " +str(4/3 * pi * r**3) + " units.")

def rect_area(l,w):
    pass

def rect_prism_vol(l, w, h):
    pass

def cone_vol(r, h):
    pass

def exit():
    sys.exit()

# Main Function

def main():
    if var.get() == "Circle Circumference":
        r = int(input("Enter the radius of the circle in the units of your choice: "))
        circ_circumf(r)

    elif var.get() == "Circle Area":
        r = int(input("Enter the radius of the circle in the units of your choice: "))
        circ_area(r)

    elif var.get() == "Sphere Volume":
        r = int(input("Enter the radius of the sphere in the units of your choice: "))
        sphere_vol(r)

# Function Button

butt1 = ttk.Button(root, text = "OK", command = main)
butt1.grid(row = 2, column = 1, padx = 5, sticky = W)

# Exit Button

butt2 = ttk.Button(root, text = "Exit", command = exit)
butt2.grid(row = 6, column = 0, padx = 5, pady = 5,  sticky = W)

root.mainloop()
\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

I think it would be instructive to compare my rank amateur code to the code that an experienced software developer would produce to accomplish the same result.

To me, the most obvious differences between experienced and novice programmers are:

  1. consideration of edge cases
  2. organization of data in a manner that lets the data drive the program's flow
  3. broadness of their working definition of the word data

In that light, here are things I'd do differently from what you have done.

Make the most of the data you've got

You're silently truncating the user's input with int(). This is bad! Mangling user data is necessary sometimes and to be avoided otherwise. Especially here, where the change will affect the output in surprising and subtle ways.

You're displaying results with maximum "precision" but basing them on a value of pi that has only four significant figures. This is misleading. Define pi with more digits, and shorten the result so that it shows the actual precision.

Separate content from presentation

A common pattern arises when you create something that's useful to someone (yourself, usually), later it becomes useful to other people, and those people need the output in a different format. Not only do you have to change the code, but it's been months since you looked at it! You won't remember how everything works. Make it easy to affect how output is displayed. Do this by keeping results in a structure that preserves the semantics of the data, and defer formatting to the very end.

Think about the task of using your code to make a web page: it's going to be hairy because your math functions have print statements in them. It's better if those functions return numbers to be printed later by formatter function.

Separate program logic from UI elements

This is the same point as the previous one, just applied to input instead of output. Use data structures that group input characteristics together, keeping display values independant from logic-affecting values.

As written, the button labels dictate the program flow (if var.get() == "Circle Circumference": etc.). If you translate your program to another language, it won't work anymore.

Functions can be data

Each kind of user-selected computation has a formula to go with it. It is natural to group those formulae alongside the other traits of the calculation.

Example

Here is one approach to your problem. Observe how the actual code has almost nothing to do with what you're calculating, or how, or what the output will look like. Instead, the data structure dictates how many variables to ask for, how to transform them into an answer, and how to format the answer for display.

From here, you could generalize this in all kinds of ways with few changes to the code. For example, float(input(prompt)) is "hardcoded" in the logic. This could be another lambda in the data, perhaps attached to each prompt, so that you can configure it to ask for integers or strings instead of just floats.

PI = 3.141592653589793
calculations = [
    dict(
        label="Circle Circumference",
        prompt=[
            "Enter the radius of the circle in the units of your choice: "
        ],
        result_format="\nThe volume of the sphere is: {0:.4f} units.",
        formula=lambda r: r*2*PI
    ),
    dict(
        label="Sphere Volume",
        prompt=[
            "Enter the radius of the sphere in the units of your choice: "
        ],
        result_format="\nThe volume of the sphere is: {0:.4f} units.",
        formula=lambda r: r**3 * 4/3 * PI
    ),
    dict(
        label="Cone Volume",
        prompt=[
            "Enter the radius of the cone in the units of your choice: ",
            "Enter the height of the cone in those same units: "
        ],
        result_format="\nThe volume of the cone is: {0:.4f} units.",
        formula=lambda r,h: r**2 * h * PI / 3
    ),
    # ... etc.
]

# selected_menu = var.get()
selected_menu="Cone Volume"  # hardcoded for demonstration purposes

action = next( filter(lambda x: x['label'] == selected_menu, calculations) )

inputs = list( map( lambda prompt: float(input(prompt)), action['prompt'] ))

answer = action['formula'](*inputs)

result_text = action['result_format'].format(answer)

print(result_text)
\$\endgroup\$
2
\$\begingroup\$

To continue on from @OhMyGoodness's excellent answer, one way you can take this is in the direction of Object Oriented Programming. You have objects, shapes, which have properties like an area and a circumference or volume. So make them into classes:

PI = 3.1415926

class Shape:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return self.name

All other shapes can inherit from this:

class Circle(Shape):
    def __init__(self, radius):
        super().__init__("Circle")
        self.radius = radius

    @property
    def area(self):
        return PI * self.radius **2

    @property
    def circumference(self):
        return 2* PI * self.radius

This has the advantage that for shapes that are special shapes of some other shape you can save a lot of repetition:

class Rectangle(Shape):
    def __init__(self, height, width):
        super().__init__("Rectangle")
        self.height, self.width = height, width

    @property
    def area(self):
        return self.height * self.width

    @property
    def circumference(self):
        return 2*self.height + 2*self.width

class Square(Rectangle):
    def __init__(self, width):
        super().__init__(width, width)
        self.name = "Square"

You can even use elements of a lower dimension for shapes in a higher dimension, where applicable:

class Cone(Shape):
    def __init__(self, radius, height):
        super().__init__("Cone")
        self.radius, self.height = radius, height
        self.base = Circle(radius)

    @property
    def area(self):
        return self.base.area + PI * self.radius * sqrt(self.radius**2 + self.height**2)

    @property
    def volume(self):
        return self.base.area * self.height / 3

And for the menu you can use introspection:

# initialize some shape:
shape = Square(2)

# You could setup buttons with this information instead of printing it:
print("Available methods:")
available = set(filter(lambda m: not m.startswith("__"), dir(shape)))
for x in available:
    print(x)

# Let the user choose (or for you click on the button):
choice = None
while choice not in available:
    choice = input()

# print result:
print(choice, "=", getattr(shape, choice))
\$\endgroup\$

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.