python – Formula filler and calculator

You’re using the wrong data structure to contain your shapes and formulas. list_shapes is a list of lists, but the first item of every list is different from the other items in the list. Different kinds of things should be stored in the same container.

A simple change is to just store the formulas in another sub list:

list_shapes = (
    ("cilinder", ("pi*r^2*h", "2*r*h", "2*r*h + 2*pi*r^2")),
    ("kegel", ("1/3*pi*r^2*h", "pi*r*a", "pi*r*a + pi*r^2")),
    ...
    )

Alternatively, you could store the data as a mapping.

list_shapes = {
    "cilinder": ("pi*r^2*h", "2*r*h", "2*r*h + 2*pi*r^2"),
    "kegel": ("1/3*pi*r^2*h", "pi*r*a", "pi*r*a + pi*r^2"),
    ...
    }

Best would be inside your own user-defined class.

PEP 8 states that variables names, function names, and parameter names should be in snake_case, with lowercase letters only. Capital letters are reserved use with constants or class names.

With this in mind, Main should be named main.

(See Ned’s Loop like a Native talk.)

The loop structure for x in range(len(list_shapes)): is frowned upon in polite Python circles. It is considered better to loop directly over the list itself, rather than looping over a list of list indices.

Why is it frowned upon? It is slower, less efficient. As an extreme example, consider a linked list. To determine the length of the list, one would need to traverse the entire list counting the items. Then, to get the n-th item, you would start from the head, and move forward n times. With the original loop, this becomes looping from the head to the 0th item, then looping from the head to the first item, then looping from the head to the second item, and so on, yielding $O(n^2)$ performance. It is more efficient to maintain an interator which advances over the list items one at a time.

For instance, to print out the shape names (using your original list_shapes structure), one could simply write:

for shape_data in list_shapes:
    print(shape_data(0))

In your case, you want the indices as well as the shape data. To get that, we use enumerate():

for x, shape_data in enumerate(list_shapes):
    print(x, shape_data(0))

This is more efficient than the original loop, since list_shape(x) never needs to be indexed.

We can improve this further using list unpacking. Using my first alternate list_shape definition, shape_data would be a list of two items. The shape name and a formula list, instead of assigning that data to shape_data, lets assign it to two variables: shape_name and formula_list:

for x, (shape_name, formula_list) in enumerate(list_shapes):
    print(x, shape_name)

Since we are not using the formula_list at this point, we could instead assign to the throw-away variable, _:

for x, (shape_name, _) in enumerate(list_shapes):
    print(x, shape_name)

Much clearer.

You go through a lot of effort to print out the uppercase shape name centered. This functionality is built-in to Python’s .format() function.

Centred in a 40 character-wide field:

print("{:^40}".format(shape_name.upper())

Or using Python 3.6’s f-strings:

print(f"{shape_name.upper():^40}")

When SWIDTH is defined, you can use it too:

print(f"{shape_name.upper():^{SWIDTH}}")

This is wrong on so many levels:

    for x in range(len(list_shapes(figur_num)(1:))):
        print("{}. {}".format(x + 1, list_shapes(figur_num)(x + 1)))

First, you take list_shapes(figur_num) and this slice it with (1:), creating a new list in memory, from which you just compute the len(...)! You could have computed the length of the original and subtracted one, saving the Python interpreter a lot of time and memory.

Next, we have the for x in range(len(…)) structure we already mentioned.

Then, we compute x + 1 twice per loop iteration!

Lastly, insult to injury, we are looking up list_shapes(figur_num) on each iteration, plus an additional time to compute the length!

Let’s rework this:

    for index, formula in enumerate(list_shapes(figur_num)(1:), 1):
        print("{}. {}".format(index, formula))

We’ve still got the list slice, but doesn’t seem as awful here, since we are actually iterate over the items of the slice. list_shapes(figur_num) is only looked up once. And the + 1 calculation vanished from the loop entirely, because we use enumerate(..., 1) to start the enumeration value at 1.

With the improved list_shapes structure, where the shape name and formulas are separate, we can eliminate the slice, too:

    for index, formula in enumerate(list_shapes(figur_num)(1)):
        print("{}. {}".format(index, formula))

When eval(formula) raises an exception, you extract the variable name which was not defined.

What if the exception was a ZeroDivisionError, or maybe ValueError due to square-root of a negative number?

You should catch the more precise exception you can to avoid catching and attempting to recover from the wrong problem.

    try:
        print(eval(formula))
    except NameError as error:
        ...

Again, you’re going through a lot of work to extract the variable name from the error. It could be simpler:

        variable_name = str(error).split("'")(1)

When you use exec(…) or eval(…), your global and local environment are available to use and modify. This include variables like formula and formula_num and functions like Main.

You should use a clean environment for your evaluator.


   local_vars = {}
   …
   while True:
       eval(formula, None, local_vars)
       …
       exec(variable, None, local_vars)

Initially, local_vars won’t contain any variables. But as the exec(…, None, local_vars) statement gets executed, your user-defined variables will start to be added to local_vars, and used in the eval(…, None, local_vars) statement.

This means to run a different calculation, possibly with a different formula, you can re-initalize local_vars to an empty dictionary, and you will be able to enter new values for the variables.