# 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.