The Sequences & Series involved are Arithmetic and Geometric. stores and allows you to calculate using these given formulas and import fractions into micropython
Support for micropython! Micropython, however, only has a subset of functions of pythons, and it's standard library is very limited, so i had to reinvent the wheel on some things.
I'd like advice on efficiency, math, optimization, compactness, design, and general information about improving my program (it was designed for micropython).
Arithmetic:
An = a1 + (n-1)d
Sn = n/2 x (2a1 + (n-1)d) OR Sn = n/2(a + L)
Geometric:
An = a1(r)^(n-1)
Sn = (a(1-(r)^n)/(1-r)
S_infinity = a/(1-r)
Both:
- ability to find the first nth to exceed a value
from math import sqrt, log
def take_inputs(*args):
"""
takes a list of variable names, and sets it up so that that input is carried out, and
a list is returned with the numerical values, to be used for unpacking into their variables
Ex:
take_inputs('a', 'b', 'c')
a = 4
b = 2
c = 3
- [4, 2, 3] -
"""
values = []
for prompt in args:
values.append(eval(str(input(prompt + " = "))))
return values
def check_for_L():
"""
checks if an 'x' was inputted at L.
if 'x', then return None (L wasn't inputted)
if any other value, then return the float form of that value
Ex:
check_for_L()
Enter x if L not present!
Enter L: 5
- 5.0 -
"""
L = input("Enter x if L not present!\nEnter L: ")
if L == 'x':
return None
return float(L)
class Arithmetic:
def a_nth(self):
print("An = a1 + (n-1)d")
a1, n, d = take_inputs('a1', 'n', 'd')
print(round(a1 + (n - 1) * d, 5))
def sum(self):
print("Sn = n/2(2a1+(n-1)d)", "Sn = n/2(a1+L)", sep="\n")
L = check_for_L()
if not L:
a1, n, d = take_inputs('a1', 'n', 'd')
print(round((n/2) * (2 * a1 + (n - 1) * d), 5))
else:
a1, n = take_inputs('a1', 'n')
print(round((n/2) * (a1 + L), 5))
def exceed_nth(self):
print("a+(n-1)d > Value")
a1, d, value = take_inputs('a1', 'd', 'value')
n = round(( (value - a1) / d ) + 1, 5)
print("n to exceed = " + str(n))
def exceed_sum_to_nth(self):
print("n/2(2a1+(n-1)d)>Value", "n/2(a1+L)>Value", sep="\n")
L = check_for_L()
if L:
a1, value = take_inputs('a1', 'value')
n = round((2 * value) / (a1 + L), 5)
print("n to exceed = " + str(n))
else:
a1, d, value = take_inputs('a1', 'd', 'value')
n = round((-2*a1 + d + sqrt(4*(a1**2) - 4*a1*d + d**2 + 8*d*value))/(2*d), 5)
print("n to exceed = " + str(n))
class Geometric:
def a_nth(self):
print("An = ar^(n-1)")
a1, r, n = take_inputs('a1', 'r', 'n')
print(round(a1 * r ** (n - 1), 5))
def sum(self):
print("Sn = a(1-r^n)/(1-r)")
a1, r, n = take_inputs('a1', 'r', 'n')
print( round((a1 * (1 - r**n) )/(1 - r), 5))
def sum_to_infinity(self):
print("S(inf) = a/(1-r)")
a1, r = take_inputs('a1', 'r')
print(round(a1/(1-r), 5))
def exceed_nth(self):
print("ar^(n-1) > Value")
a1, r, value = take_inputs('a1', 'r', 'value')
n = round(log( r*(value/a1) ) / log(r), 5)
print("n to exceed = " + str(n))
def exceed_sum_to_nth(self):
print("a(1-r^n)/(1-r)>Value")
a1, r, value = take_inputs('a1', 'r', 'value')
n = round(log( (a1 + r*value - value) / a1) / log(r), 5)
print("n to exceed = " + str(n))
class BIOS:
choices_prompt = "a for arithmetic\nb for geometric\n>> "
def __init__(self):
"""
responsible for handling basic input and output, for the terminal, and options/choices.
"""
self.running = True
self.arithmetic = Arithmetic()
self.geometric = Geometric()
self.choices = {'a': self.arithmetic_sequences, 'b': self.geometric_sequences}
self.arithmetic_choices = {'a': self.arithmetic.a_nth, 'b': self.arithmetic.sum, 'c': self.arithmetic_exceed}
self.geometric_choices = {'a': self.geometric.a_nth, 'b': self.geometric.sum, 'c': self.geometric.sum_to_infinity, 'd': self.geometric_exceed}
self.arithmetic_exceed_choices = {'a': self.arithmetic.exceed_nth, 'b': self.arithmetic.exceed_sum_to_nth}
self.geometric_exceed_choices = {'a': self.geometric.exceed_nth, 'b': self.geometric.exceed_sum_to_nth}
@staticmethod
def menu(*args):
"""
a list of prompts is given as arguments,
and a menu like input system is made.
Ex:
menu('a for apple', 'b for bye', 'c for cat')
a for apple
b for bye
c for cat
>> a
- a -
"""
return input("\n".join(list(args) + [">> "]))
def stop_decorator(func):
"""
Decorator for stopping certain functions, after they're done by asking with a prompt
"""
def wrapper(self):
func(self)
command = input("Enter nothing to stop: ")
if command == '':
self.running = False
return wrapper
@stop_decorator
def arithmetic_sequences(self):
sub_choice = self.menu("Arithmetic:", "a for a_nth term", "b for sum", "c for min_term_exceed")
self.arithmetic_choices.get(sub_choice, lambda: None)()
@stop_decorator
def geometric_sequences(self):
sub_choice = self.menu("Geometric:", "a for a_nth term", "b for sum", "c for sum to infinity", "d for min terms exceed")
self.geometric_choices.get(sub_choice, lambda: None)()
def arithmetic_exceed(self):
sub_choice = self.menu("Exceed Arithmetic:", "a for nth", "b for sum_to_nth")
self.arithmetic_exceed_choices.get(sub_choice, lambda: None)()
def geometric_exceed(self):
sub_choice = self.menu("Exceed Geometric:", "a for nth", "b for sum_to_nth")
self.geometric_exceed_choices.get(sub_choice, lambda: None)()
def main(self):
"""
runs the program indefinitely as long as the user wants.
"""
while self.running:
self.choices.get(input(self.choices_prompt), lambda: None)()
print()
program = BIOS()
program.main()
```
-
\$\begingroup\$ yes, so that i can input values like 3/2, which is a fraction, and possibly pi, in the near future. I am aware this is a vulnerability/security issue, but this is just a math program lol for a calculator nonetheless. I'm pretty much gonna be the only one using it. \$\endgroup\$Eren Yaegar– Eren Yaegar2020年09月13日 22:41:38 +00:00Commented Sep 13, 2020 at 22:41
1 Answer 1
As a deleted comment probably said, eval
has risk. That risk can be mitigated, depending on how much effort you want to put in and how security-sensitive the application is. I demonstrate with a constrained compile
/eval
; a more paranoid approach would validate an AST.
so that i can input values like 3/2, which is a fraction, and possibly pi
Keep in mind that entering a fraction-like string of 3/2 and evaluating it to a float is not the same as a true fraction. I will assume the former and not the latter.
Support for micropython!
Great, I guess; but I don't have a micropython environment so my demonstration makes simple use of modern CPython.
Consider replacing values.append
with an iterator.
This is a strange choice:
"Enter x if L not present!\nEnter L: "
I would sooner just accept a blank string (from a return keystroke) to indicate "not present".
check_for_L
returns a string coerced to a float; but why not leverage the same eval
machinery?
Arithmetic
and Geometric
exhibit a classic antipattern of class-as-namespace. There are no members - convert these to modules if you want namespaces.
BIOS
is a slightly obscure name, and again, there are no (justifiable) members; this can also be converted to a module. In terms of members,
running
is a loop-flag anti-pattern that can just be replaced with a predicate on an inputarithmetic
andgeometric
will no longer be objectschoices
can be passed inline to amenu
function. These are not so expensive as to warrant caching.
Instead of repeating calls to round
all with the same precision, make a utility function. Note that round
(a) is not needed for integers, (b) is not enough to remove the .0
default suffix for whole-number floats, and (c) will not function with complex numbers; so you will benefit from code that addresses all of these cases.
Some of your numeric expressions have redundant parens or can be simplified; especially,
log( (a1 + r*value - value) / a1) / log(r)
can be
math.log(1 + value*(r - 1)/a1, r)
menu
can be rewritten so that it generates the a for
, b for
, etc. automatically.
Code like
input("\n".join(list(args) + [">> "]))
is a little convoluted; why not just have the input prompt be the >>
alone, preceded by simple print
s?
stop_decorator
is way more complicated than it needs to be. This and self.running
can go away and be replaced with a simple check in a loop.
get(sub_choice, lambda: None)
is both more tricky than it should be and more quiet on failure than it should be. You can instead loop on a user's input until they enter a valid option.
Add a __main__
guard.
All together,
import builtins
import math
import numbers
import string
import typing
if typing.TYPE_CHECKING:
# Don't use numbers.Number: it's missing most of its operand specifiers
Number = numbers.Real | numbers.Complex
MATH_GLOBALS = {
# Built-ins: only math-like functions
'__builtins__': {
k: getattr(builtins, k)
for k in (
'abs', 'complex', 'divmod', 'float', 'int', 'max', 'min', 'pow', 'round', 'sum',
)
}
} | {
# Globals: only public symbols from the math module
k: v
for k, v in math.__dict__.items()
if not k.startswith('_')
}
def eval_math(expr: str) -> 'Number':
"""
eval() expr as a math-like expression, with the following safety restrictions:
- 'eval' mode (no function definitions or control statements, etc.)
- most built-ins (input, exec, etc.) removed, other than math-like built-ins such as `abs`
- for globals, only the public symbols from the math module like `sin` and `pi`
- no locals
"""
code = compile(
source=expr, filename='<expression>', mode='eval',
flags=0, optimize=0, dont_inherit=True,
)
value = eval(code, MATH_GLOBALS, {}) # no locals
if not isinstance(value, (numbers.Real, numbers.Complex)):
raise TypeError(f'{value!r} is not a number')
return value
def take_inputs(*args: str) -> 'typing.Iterator[Number]':
"""
For each arg, loop until a valid input is captured, safe(ish) eval it, and yield its value
"""
print('Enter variables (expressions supported):')
for name in args:
while True:
expr = input(f'{name} = ')
try:
yield eval_math(expr)
break
except (ValueError, NameError, SyntaxError, TypeError) as e:
print(str(e))
def check_for_L() -> 'Number | None':
"""
Loop until user either enters an empty string (coerced to None), or a valid input:
safe(ish) eval it and return its value
"""
while True:
expr = input('Enter L expression, or return for none: ')
if expr == '':
return None
try:
return eval_math(expr)
except (ValueError, NameError, SyntaxError, TypeError) as e:
print(str(e))
def trunc(x: 'Number', ndigits: int = 5) -> str:
"""
Format x as a string with precision up to ndigits. This is different from f'{x:.5f}' where
ndigits will always be shown regardless of whether they're zero.
"""
match x:
case int():
return str(x)
case float():
x = round(x, ndigits=ndigits)
if x.is_integer():
return f'{x:.0f}'
return str(x)
case complex():
isign = math.copysign(1, x.imag)
sign_char = '+' if isign >= 0 else '-'
return f'{trunc(x.real, ndigits)} {sign_char} {trunc(isign*x.imag, ndigits)}j'
case _:
raise TypeError(f'{x!r} is not a number')
def arithmetic_a_nth() -> None:
print('An = a1 + (n-1)d')
a1, n, d = take_inputs('a1', 'n', 'd')
print(trunc(a1 + (n - 1)*d))
def arithmetic_sum() -> None:
print('Sn = n/2(2a1+(n-1)d)')
print('Sn = n/2(a1+L)')
L = check_for_L()
if L is None:
a1, n, d = take_inputs('a1', 'n', 'd')
print(trunc(n/2*(2*a1 + (n - 1)*d)))
else:
a1, n = take_inputs('a1', 'n')
print(trunc(n/2*(a1 + L)))
def arithmetic_exceed_nth() -> None:
print('a+(n-1)d > Value')
a1, d, value = take_inputs('a1', 'd', 'value')
print('n to exceed =', trunc((value - a1)/d + 1))
def arithmetic_exceed_sum_to_nth() -> None:
print('n/2(2a1+(n-1)d)>Value')
print('n/2(a1+L)>Value')
L = check_for_L()
if L is not None:
a1, value = take_inputs('a1', 'value')
n = 2*value/(a1 + L)
else:
a1, d, value = take_inputs('a1', 'd', 'value')
n = (-2*a1 + d + math.sqrt(4*a1**2 - 4*a1*d + d**2 + 8*d*value))/(2*d)
print('n to exceed =', trunc(n))
def geometric_a_nth() -> None:
print('An = ar^(n-1)')
a1, r, n = take_inputs('a1', 'r', 'n')
print(trunc(a1 * r**(n - 1)))
def geometric_sum() -> None:
print('Sn = a(1-r^n)/(1-r)')
a1, r, n = take_inputs('a1', 'r', 'n')
print(trunc(a1*(1 - r**n)/(1 - r)))
def geometric_sum_to_infinity() -> None:
print('S(inf) = a/(1-r)')
a1, r = take_inputs('a1', 'r')
print(trunc(a1/(1 - r)))
def geometric_exceed_nth() -> None:
print('ar^(n-1) > Value')
a1, r, value = take_inputs('a1', 'r', 'value')
n = math.log(r*value/a1, r)
print('n to exceed =', trunc(n))
def geometric_exceed_sum_to_nth() -> None:
print('a(1-r^n)/(1-r)>Value')
a1, r, value = take_inputs('a1', 'r', 'value')
n = math.log(1 + value*(r - 1)/a1, r)
print('n to exceed =', trunc(n))
def menu(options: 'dict[str, typing.Callable[[], None]]') -> None:
"""
Print a prompt like
a for sum
b for min_term_exceed
>
where the letters are enumerated automatically, the names are the keys of `options`, and
the corresponding value from `options` is called once the user inputs a valid letter.
"""
choices = dict(zip(string.ascii_lowercase, options.keys()))
print('\n'.join(
f'{letter} for {name}'
for letter, name in choices.items()
))
while True:
selection = input('> ')
if selection in choices:
break
options[choices[selection]]()
def arithmetic_sequences() -> None:
print('Arithmetic:')
menu({'a_nth': arithmetic_a_nth, 'sum': arithmetic_sum, 'min term exceed': arithmetic_exceed})
def geometric_sequences() -> None:
print('Geometric:')
menu({'a_nth': geometric_a_nth, 'sum': geometric_sum, 'sum to infinity': geometric_sum_to_infinity, 'min term exceed': geometric_exceed})
def arithmetic_exceed() -> None:
print('Exceed Arithmetic:')
menu({'nth': arithmetic_exceed_nth, 'sum to nth': arithmetic_exceed_sum_to_nth})
def geometric_exceed() -> None:
print('Exceed Geometric:')
menu({'nth': geometric_exceed_nth, 'sum to nth': geometric_exceed_sum_to_nth})
def main() -> None:
while True:
menu({'arithmetic': arithmetic_sequences, 'geometric': geometric_sequences})
if input('Exit [y]/n? ') != 'n':
break
print()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
pass # silent ctrl+C break
Output demonstrating pi
and fraction-like input:
a for arithmetic
b for geometric
> b
Geometric:
a for a_nth
b for sum
c for sum to infinity
d for min term exceed
> d
Exceed Geometric:
a for nth
b for sum to nth
> b
a(1-r^n)/(1-r)>Value
Enter variables (expressions supported):
a1 = pi
r = 5/3
value = exp(5)
n to exceed = 6.81458
Exit [y]/n?
Output demonstrating complex numbers:
a for arithmetic
b for geometric
> a
Arithmetic:
a for a_nth
b for sum
c for min term exceed
> a
An = a1 + (n-1)d
Enter variables (expressions supported):
a1 = 3.2
n = 1/2
d = 10 + 12j
-1.8 - 6j
Exit [y]/n?
Explore related questions
See similar questions with these tags.