The reason your solution is convoluted is that the problem hamstringing you by not allowing any modules (except sys
) including the built in ones!
Secondly it seems you need to freshen up on your basics. For instance this
def lookup(word):
if word in d_define:
return d_define[word]
else:
return "unknown"
Can be written as
d_define.get(word, "unknown")
Secondly d_define
is a bad name; as a general rule avoid abbreviations in variable and function names. Assume that everything is fair game, no limitations on external libraries. The crux is that eval
is really dangerous, and you should never run it on user input. For instance an user could import the os
library and then run commands to delete your whole file system. However, there are safe ways to use it, for instance
SAFE_CHARS = set("0123456789+-*(). /")
for char in expr:
if char not in SAFE_CHARS:
value = "unknown"
break
else:
value = eval(expr)
If we detect any "malicious intent", e.g anything other that mathematical expressions we break and return "unknown"
. We first replace all the words with their values before evaluating. Some care is taken here for instance if we have "foobar=3" and "bar=2" then "foobar" must be replaced before "bar" otherwise we might end up with "foo2".
Note that this way of solving the problem is very slow with many definitions, but I really do not think speed matters.
import sys
SAFE_CHARS = set("0123456789+-*(). /")
class Calculator:
def __init__(self):
self.definitions = dict()
self.values = dict()
def definition(self, name: str, integer: float) -> None:
self.definitions[name] = integer
self.values[integer] = name
def clear(self):
self.definitions = dict()
self.values = dict()
def calculate(self, expr: str) -> float | str:
expr = "".join(expr.split()) # Remove all spaces
definitions = sorted(
self.definitions.items(), key=lambda x: len(x[0]), reverse=True
)
for (word, value) in definitions:
expr = expr.replace(word, str(value))
if all(c in SAFE_CHARS for c in char):
value = eval(expr)
else:
value = "unknown"
return self.values.get(value, "unknown")
if __name__ == "__main__":
calc = Calculator()
for line in sys.stdin.readlines():
if not (line := line.strip()):
continue
if line.startswith("def"):
_, name, value = line.split()
calc.definition(name, float(value))
elif line.startswith("calc") and line.endswith("="):
_, expr = line[:-1].split("calc")
print(f"{line} {calc.calculate(expr)}")
elif line.startswith("clear"):
calc.clear()
Assuming that eval
is off the table, I would probably rewrite it as follows. Due note that we use a recursive algorithm just for fun to evaluate the expressions. A lot of care is taken to correctly handle deeply nested expressions.
import sys
def is_number(s: str) -> bool:
try:
float(s)
return True
except ValueError:
return False
class Calculator:
def __init__(self):
self.definitions = dict()
self.values = dict()
def definition(self, name: str, integer: float) -> None:
self.definitions[name] = integer
self.values[integer] = name
def clear(self):
self.definitions = dict()
self.values = dict()
def calculate(self, expr: str) -> float | str:
expr = "".join(expr.split()) # Remove all spaces
try:
value = self._eval(expr)
except KeyError:
value = "unknown"
return self.values.get(value, "unknown")
def _eval(self, expr: str) -> float:
"""self.evaluates an expression consisting of binary expressions
>>> calculator._eval("123.5")
123.5
>>> calculator._eval("(5 + 3) + (1 + (2 + 6))")
17.0
>>> calculator._eval("((((5))+(2))+1+2)")
10.0
"""
if is_number(expr):
return float(expr)
if "(" in expr:
start, stop = inner_expression(expr)
new_expr = self._eval(expr[start + 1 : stop - 1])
expr = expr.replace(expr[start:stop], str(new_expr))
return self._eval(expr)
elif "+" in expr:
expr_1, expr_2 = expr.split("+", 1)
return self._eval(expr_1) + self._eval(expr_2)
elif "-" in expr:
expr_1, expr_2 = expr.split("-", 1)
return self._eval(expr_1) - self._eval(expr_2)
elif "/" in expr:
expr_1, expr_2 = expr.split("/", 1)
return self._eval(expr_1) / self._eval(expr_2)
elif "*" in expr:
expr_1, expr_2 = expr.split("*", 1)
return self._eval(expr_1) * self._eval(expr_2)
elif "^" in expr:
expr_1, expr_2 = expr.split("^", 1)
return self._eval(expr_1) ** self._eval(expr_2)
return self.definitions[expr]
MATCHING_BRACKET = {")": "(", "}": "{", "[": "]"}
OPEN_BRACKETS = set(["(", "{", "["])
def inner_expression(expr: str):
"""Returns the indices for the inner most expression
>>> inner_expression("(5}")
Traceback (most recent call last):
ValueError: Incompatible brackets '}' does not match '('!
>>> inner_expression("((5)")
Traceback (most recent call last):
ValueError: Incomplete brackets found, the brackets ['('] was never closed!
>>> a, b = inner_expression('((5+2)+(1+2))'); '((5+2)+(1+2))'[a:b]
'(5+2)'
>>> a, b = inner_expression('((((5))+(2))+1+2)'); '((((5))+(2))+1+2)'[a:b]
'(5)'
"""
stack = []
deepest_nesting = start = 0
indices = None
for i, char in enumerate(expr):
if char in MATCHING_BRACKET:
if stack[-1] != MATCHING_BRACKET[char]:
raise ValueError(
f"Incompatible brackets '{char}' does not match '{stack[-1]}'!"
)
if (nesting := len(stack)) > deepest_nesting:
deepest_nesting = nesting
indices = (start, i + 1)
stack.pop()
elif char in OPEN_BRACKETS:
stack.append(char)
start = i
if stack:
raise ValueError(
f"Incomplete brackets found, the brackets {stack} was never closed!"
)
return indices if indices is not None else (0, len(expr))
if __name__ == "__main__":
import doctest
doctest.testmod(extraglobs={"calculator": Calculator()})
calc = Calculator()
for line in sys.stdin.readlines():
if not (line := line.strip()):
continue
if line.startswith("def"):
_, name, value = line.split()
calc.definition(name, float(value))
elif line.startswith("calc") and line.endswith("="):
_, expr = line[:-1].split("calc")
print(f"{line} {calc.calculate(expr)}")
elif line.startswith("clear"):
calc.clear()
EDIT: If we allow ourselves to use the full capabilities of Python, disregarding that our code must be beginner friendly we can do as shown below
Reduce was introduced to reduce the number of recursive calls and offer a small speed improvement. Our original code reduced the expression as follows a + b + c + d + e
= a + (b + c + d+ e)
= a a + (b + (c + d + e))
usw.
The operators were extracted into a seperate function reducing this
elif "+" in expr:
expr_1, expr_2 = expr.split("+", 1)
return self._eval(expr_1) + self._eval(expr_2)
elif "-" in expr:
expr_1, expr_2 = expr.split("-", 1)
return self._eval(expr_1) - self._eval(expr_2)
elif "/" in expr:
expr_1, expr_2 = expr.split("/", 1)
return self._eval(expr_1) / self._eval(expr_2)
elif "*" in expr:
expr_1, expr_2 = expr.split("*", 1)
return self._eval(expr_1) * self._eval(expr_2)
elif "^" in expr:
expr_1, expr_2 = expr.split("^", 1)
return self._eval(expr_1) ** self._eval(expr_2)
return self.definitions[expr]
into this
for operator, function in OPERATORS.items():
if operator not in expr:
continue
expressions = map(self._eval, expr.split(operator))
return reduce(function, expressions)
Minor cleanup for the inner_expression
as well
A bit more typing hints and doctests
Code without imports
import sys
def is_number(s: str) -> bool:
try:
float(s)
return True
except ValueError:
return False
OPERATORS = {
"+": lambda x, y: x + y,
"-": lambda x, y: x - y,
"/": lambda x, y: x / y,
"*": lambda x, y: x * y,
"^": lambda x, y: x**y,
}
def reduce(function, iterable, initializer=None):
"""Implements the reduce function without external modules
>>> reduce(lambda x, y: x + y, range(5))
10
>>> reduce(lambda x, y: x + y, range(5), initializer=100)
110
>>> reduce(lambda x, y: f"({x}) + {y}", range(5))
'((((0) + 1) + 2) + 3) + 4'
>>> reduce(lambda x, y: f"({x}) + {y}", range(5), initializer=100)
'(((((100) + 0) + 1) + 2) + 3) + 4'
"""
it = iter(iterable)
if initializer is None:
value = next(it)
else:
value = initializer
for element in it:
value = function(value, element)
return value
class Calculator:
def __init__(self) -> None:
self.definitions = dict()
self.values = dict()
def definition(self, name: str, integer: float) -> None:
self.definitions[name] = integer
self.values[integer] = name
def clear(self) -> None:
self.definitions = dict()
self.values = dict()
def calculate(self, expr: str) -> float | str:
expr = "".join(expr.split()) # Remove all spaces
try:
value = self._eval(expr)
except KeyError:
value = "unknown"
return self.values.get(value, "unknown")
def _eval(self, expr: str) -> float:
"""self.evaluates an expression consisting of binary expressions
>>> qalc._eval("123.5")
123.5
>>> qalc._eval("(5 + 3) + (1 + (2 + 6))")
17.0
>>> qalc._eval("((((5))+(2))+1+2)")
10.0
"""
if is_number(expr):
return float(expr)
if "(" in expr:
start, stop = inner_expression(expr)
new_expr = self._eval(expr[start + 1 : stop - 1])
expr = expr.replace(expr[start:stop], str(new_expr))
return self._eval(expr)
for operator, function in OPERATORS.items():
if operator not in expr:
continue
expressions = map(self._eval, expr.split(operator))
return reduce(function, expressions)
return self.definitions[expr]
MATCHING_BRACKET = {")": "(", "}": "{", "[": "]"}
OPEN_BRACKETS = set(["(", "{", "["])
def inner_expression(expr: str) -> tuple[int, int]:
"""Returns the indices for the inner most expression
>>> inner_expression("(5}")
Traceback (most recent call last):
ValueError: Incompatible brackets '}' does not match '('!
>>> inner_expression("((5)")
Traceback (most recent call last):
ValueError: Incomplete brackets found, the brackets ['('] was never closed!
>>> a, b = inner_expression('((5+2)+(1+2))'); '((5+2)+(1+2))'[a:b]
'(5+2)'
>>> a, b = inner_expression('((((5))+(2))+1+2)'); '((((5))+(2))+1+2)'[a:b]
'(5)'
"""
stack = []
deepest_nesting = start = 0
indices = None
for i, char in enumerate(expr):
if char in OPEN_BRACKETS:
stack.append(char)
start = i
continue
elif char not in MATCHING_BRACKET:
continue
matching_brackets = stack[-1] == MATCHING_BRACKET[char]
if not matching_brackets:
raise ValueError(
f"Incompatible brackets '{char}' does not match '{stack[-1]}'!"
)
if (nesting := len(stack)) > deepest_nesting:
deepest_nesting = nesting
indices = (start, i + 1)
stack.pop()
if stack:
raise ValueError(
f"Incomplete brackets found, the brackets {stack} was never closed!"
)
if indices is None:
return (0, len(expr))
return indices
def first_and_remaining(line: str) -> tuple[str, str | None]:
"""If lines = 'a b c' this returns ('a', 'b c')
>>> first_and_remaining('a b c')
('a', 'b c')
>>> first_and_remaining('')
('', None)
>>> first_and_remaining(' a ')
('a', None)
>>> first_and_remaining(' a b ')
('a', 'b')
"""
if " " not in (stripped := line.strip()):
return stripped, None
return tuple(x.strip() for x in stripped.split(maxsplit=1))
def calculator(lines, calc=None) -> None:
"""Feeds lines into a calculator
>>> commands = '''
... def foo 3
... calc foo + bar =
... def bar 7
... def programming 10
... calc foo + bar =
... def is 4
... def fun 8
... calc programming - is + fun =
... def fun 1
... calc programming - is + fun =
... clear'''
>>> calculator(commands.splitlines())
calc foo + bar = unknown
calc foo + bar = programming
calc programming - is + fun = unknown
calc programming - is + fun = bar
"""
if calc is None:
calc = Calculator()
for command, expression in map(first_and_remaining, lines):
if not command:
continue
if expression is None:
if command == "clear":
calc.clear()
continue
if command == "def":
name, value = expression.split()
calc.definition(name, int(value))
elif command == "calc" and expression.endswith("="):
value = calc.calculate(expression[:-1])
print(f"{command} {expression} {value}")
if __name__ == "__main__":
import doctest
doctest.testmod(extraglobs={"qalc": Calculator()})
calculator(sys.stdin.readlines())