Let's proceed top-down. I'll suggest cleverer ways to do a bunch of stuff, but the review will culminate in the two most important points:
- this is a huge security hole, and
- you should organize your code into functions.
Introductory message
A good way to write a multiline string is using """the longstring"""
syntax.
In this case, though, that introductory message could just as well be the docstring for the program. You could use the special __doc__
variable to fetch the docstring by introspection.
"""This program reads boolean expressions and outputs the truth table.
You may use the operators ~, ∨, and ∧, or
their text equivalents, not, or, and and.
You may use parenthesis, braces, and brackets to group your expressions,
and you must use single letters for your variable names, as 'a∨b'"""
print(__doc__)
Note that "equation" is not the right word: you don't want a string with an equals sign. You just want a "boolean expression".
Canonicalizing the expression
Doing string substitutions in multiple passes is bad practice. In the best case, you waste a bit of effort making multiple passes over the string. In the worst case, passing previously substituted text through another round could be incorrect — though fortunately that's not the case here.
I recommend doing the substitution in one pass using a regular expression.
REPLACEMENTS = {
'~': ' not ',
'v': ' or ',
'^': ' and ',
'[': '(',
']': ')',
'{': '(',
'}': ')',
}
expr = re.sub('|'.join(re.escape(sym) for sym in REPLACEMENTS.keys()),
lambda sym: REPLACEMENTS[sym.group(0)],
expr).strip()
Extracting the variables
This, too, can be simplified using a regular expression. \b[A-Za-z]\b
looks for any single letter bounded by word boundaries on both sides.
In addition, deduplication is more elegant using a set
.
vars = sorted(set(re.findall(r'\b[A-Za-z]\b', expr)))
Generating the boolean values
This is a perfect job for itertools.product()
, and in fact the documentation has just such an example:
product(range(2), repeat=len(vars))
Evaluating the expression
First of all, you should use eval()
, not exec()
, since you want to evaluate the expression and get its value, rather than running some code for its side-effect. result = eval(expr)
is better than exec('result = ' + expr)
.
Most importantly, though, calling eval()
or exec()
like that is a huge security hole — it lets the user execute arbitrary Python code. For example, the user might enter this to be executed:
__import__('os').system('rm -rfi /')
The consensus is that Python can't be safely sandboxed. We can try to do better by evaluating the expression in a scope that contains just the relevant variables.
NO_GLOBALS = {'__builtins__': {}}
for vals in product(range(2), repeat=len(vars)):
locals = dict(zip(vars, vals))
result = eval(expr, NO_GLOBALS, locals)
However, I suspect that even that won't be secure. The lambda
keyword is still available, for example, and that makes me nervous. Ultimately, the only safe approach may be to write your own code to parse and evaluate the expression — Python itself is too powerful.
Suggested solution
As a final remark: the problem can be divided neatly into functions. Naming these chunks of work make your code easier to follow.
"""This program reads boolean expressions and outputs the truth table.
You may use the operators ~, ∨, and ∧, or
their text equivalents, not, or, and and.
You may use parenthesis, braces, and brackets to group your expressions,
and you must use single letters for your variable names, as 'a∨b'"""
from itertools import product
import re
def canonicalize(expr):
REPLACEMENTS = {
'~': ' not ',
'v': ' or ',
'^': ' and ',
'[': '(',
']': ')',
'{': '(',
'}': ')',
}
return re.sub('|'.join(re.escape(sym) for sym in REPLACEMENTS.keys()),
lambda sym: REPLACEMENTS[sym.group(0)],
expr).strip()
def extract_variables(expr):
return sorted(set(re.findall(r'\b[A-Za-z]\b', expr)))
def truth_table(expr):
expr = canonicalize(expr)
vars = extract_variables(expr)
NO_GLOBALS = {'__builtins__': {}}
# Print header
print('\t'.join(vars + [expr]))
# Print body
for vals in product(range(2), repeat=len(vars)):
locals = dict(zip(vars, vals))
result = eval(expr, NO_GLOBALS, locals)
print('\t'.join([str(v) for v in vals] + [str(result)]))
def prompt_expr():
print(__doc__)
print()
return input()
if __name__ == '__main__':
truth_table(prompt_expr())