patternpythonMinor
Truth Table Calculator
Viewed 0 times
calculatortabletruth
Problem
In of my software engineering classes, I had to write a truth table calculator in the language of my choice. This is what I came up with, and I would like to know what I should do better next time:
I have a working version at IdeOne.
import copy
import sys
print("This program reads boolean equations and outputs the truth table.")
print("You may use the operators ~, ∨, and ∧, or")
print("their text equivalents, not, or, and and.")
print("You may use parenthesis, braces, and brackets to group your equations,")
print("and you must use single letters for your variable names, as 'a∨b'\n")
equation = input()
equation_copy = equation
equation_vars = []
equation = " " + equation + " "
equation = equation.replace("~", " not ")
equation = equation.replace("∨", " or ")
equation = equation.replace("∧", " and ")
equation = equation.replace("[", "(")
equation = equation.replace("]", ")")
equation = equation.replace("{", "(")
equation = equation.replace("}", ")")
for index in range(len(equation)):
if equation[index] in " ()^":
continue
if equation[index + 1] in " ()" and equation[index - 1] in " ()" and equation[index] not in equation_vars:
equation_vars.append(equation[index])
equation_vars.sort()
for equationIndex in equation_vars:
sys.stdout.write(equationIndex + '\t')
print(equation_copy)
for equationIndex in range(pow(2,len(equation_vars))):
vals = str(bin(equationIndex)).replace('0b','').zfill(len(equation_vars))
for value in vals:
sys.stdout.write(value + '\t')
equation_vars_copy = copy.deepcopy(equation_vars)
for index in range(len(equation_vars_copy)):
equation_vars_copy[index] += " = " + vals[index]
exec(equation_vars_copy[index])
truthTable = "result = equation"
truthTable = truthTable.replace('equation', equation)
exec(truthTable)
if result:print(1)
else:print(0)I have a working version at IdeOne.
Solution
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:
Introductory message
A good way to write a multiline string is using
In this case, though, that introductory message could just as well be the docstring for the program. You could use the special
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.
Extracting the variables
This, too, can be simplified using a regular expression.
In addition, deduplication is more elegant using a
Generating the boolean values
This is a perfect job for
Evaluating the expression
First of all, you should use
Most importantly, though, calling
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.
However, I suspect that even that won't be secure. The
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 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())Code Snippets
"""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__)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()vars = sorted(set(re.findall(r'\b[A-Za-z]\b', expr)))product(range(2), repeat=len(vars))__import__('os').system('rm -rfi /')Context
StackExchange Code Review Q#75775, answer score: 4
Revisions (0)
No revisions yet.