3

I am currently trying to work on a code that takes input from a user asking them to open a file with math problems within it, and then outputs a file with the answers to those problems. I have looked all over for some kind of solution to this, but these are the closest I have gotten:

text file contents:

2 + 2

3 * 3

4.5 - 2.3

-8.8 + 1.2

2 ^ 4

8.9 / 2.3

5 % 3

-2 * -2

Attempt 1:

input_file_name = input("What file would you like to open? ")
output_file_name = input("What file would you like to write to? ")

with open(input_file_name,"r") as input_file:
    contents = input_file.readlines()
    num = "".join(contents)
    contents_length = len(num)

with open(output_file_name, "w") as output_file:
    while contents_length >= 0:
        num = num.replace("^","**") # change the ^ to ** so python can properly do the function
        contents_value = exec(num)

        contents_length = contents_length - 1

    output_file.write(str(contents_value))

The text file I received would return "None"

Attempt 2:

input_file_name = input("What file would you like to open? ")
output_file_name = input("What file would you like to write to? ")

infile = open(input_file_name, "r")
outfile = open(output_file_name, "w")

lines = infile.readlines()

i = len(lines) - 1

while i >= 0:
    ans = eval(lines[i])
    outfile.write(str(ans))
    i = i - 1

infile.close()
outfile.close()

And the text document had 423.86956521739130476-7.60000000000000052.294 in the file.

So, I'm not really sure what else to do. Any help will be greatly appreciated.

bcochran
  • 43
  • 5

4 Answers4

1

You need to process each line separately, but this code

num = "".join(contents)

combines all the lines into a single string.

The other problem is the unwanted decimal places. You can fix that using the round function. The code below reads the data directly from the script, but it's easy enough to adapt it to reading the data from the file, either with .readlines, or by simply looping over the file object.

data = '''\
2 + 2
3 * 3
4.5 - 2.3
-8.8 + 1.2
2 ^ 4
8.9 / 2.3
5 % 3
-2 * -2
'''.splitlines()

for line in data:
    line = line.replace('^', '**')
    s = round(eval(line), 6)
    print(s)

output

4
9
2.2
-7.6
16
3.869565
2
4

That output was created using Python 3. On Python 2, it's not quite as clean: everything will be written as a float.

Python 2 output

4.0
9.0
2.2
-7.6
16.0
3.869565
2.0
4.0

I assume that you're aware that eval and exec should generally be avoided because they can be a security risk. For details, please see Eval really is dangerous by SO veteran Ned Batchelder. But I guess you don't have much choice here, and you can guarantee that the input file won't contain malicious code.

PM 2Ring
  • 54,345
  • 6
  • 82
  • 182
  • Ah got! Thank you so much! – bcochran Dec 02 '17 at 22:28
  • I was not aware, I had actually learned about eval and exec today, haha. But thanks for the heads up! – bcochran Dec 02 '17 at 22:31
  • What if I wanted the output file to have the original expression + = + answer? – bcochran Dec 02 '17 at 22:40
  • @bcochran One way is to combine the two strings using formatting: `print('{} = {}'.format(line, s))`. Another way is to use the `sep` argument of `print`, eg: `print(line, s, sep=' = ')`. BTW, `print` takes a `file` arg so you can use it to print directly to an open text file. – PM 2Ring Dec 02 '17 at 22:54
0

you almost got the right answer in your second attempt. Just add a new line character each time you are writing the content to your file and I think it will get properly formatted

outfile.write(str(ans) + '\n')

PS I have not tested it, please check for any syntax errors

Ezio
  • 2,837
  • 2
  • 29
  • 45
  • I tried this, and it did put all the "answers" on their own line, but only the answer to the first problem was right. Thank you though, another step closer! Edit: Actually, the first, 4th, and last answers were correct. – bcochran Dec 02 '17 at 22:16
  • Edit: The answers were right, just in the wrong order, haha. – bcochran Dec 02 '17 at 22:30
  • I dont understand, why the answers were in incorrect order? – Ezio Dec 03 '17 at 05:49
  • I think it was because of the way I had the program check the lines? I got the second attempt from an example we did in class in which my professor had us write a program that basically took a file and flipped the contents in it. – bcochran Dec 04 '17 at 13:32
0

Yes, you're right that you want to use eval() not exec().

it looks like you just need to write a newline after each solution i.e.

outfile.write('\n')
0

For the sake of argument I want to demonstrate that you don't have to use eval at all for your problem! As PM 2Ring has warned, using eval or exec is a security hazard in case there's any chance that the string you're evaluating can be tampered with by malicious third parties. Many people dismiss this security risk, but if you read my favourite answer in the subject you can see a strikingly simple example to executing arbitrary programs outside python with a call to eval. So one should never use eval/exec on sources that are not 100% trusted and under control.

Coming back to your problem, your examples make it seem that the pattern is always val1 OP val2, i.e. two numbers (possibly negative, possibly floats) with an infix mathematical operator. This structure is so simple that you can (and I believe should) write your own parser for it. This serves as a demonstration that by explicitly handling inputs we expect in a safe way we can guarantee that our program breaks on invalid and malicious input.

Here's a function that returns the result for one line (one expression) of your input:

import operator

def evaluate(expr):
    '''Evaluate expressions of the form `val1 OP val2`; OP is in [+,-,/,*,%,^]'''
    ops = {
        '+': operator.add,
        '-': operator.sub,
        '*': operator.mul,
        '/': operator.truediv,
        '%': operator.mod,
        '^': operator.pow,
        }
    # gather operators and put minus last so we can handle negative numbers right
    opskeys = list(ops)
    ind = opskeys.index('-')
    implemented_ops = opskeys[ind+1:] + opskeys[:ind+1]

    for opnow in implemented_ops:
        if opnow in expr:
            val1,op,val2 = expr.rpartition(opnow)
            # convert the numbers to floats, or ints in case they are integral
            val1,val2 = (int(val) if val.is_integer() else val
                         for val in map(float,[val1,val2]))
            break
    else:
        raise ValueError('Invalid input line "{}"!'.format(expr))

    return ops[op](val1,val2) # or round(...,digits) see PM 2Ring's answer

The function takes a single line of input, finds the appropriate mathematical operator, splits the line using that character, then evaluates the result. It uses a dispatch dict to choose actual mathematical operators based on the characters; one upside is that we don't even have to convert the power operator. We convert integer inputs to integers, and leave the rest as floats.

The function handles subtraction last, this ensures that negative numbers don't mess up calculations with every other operator. This isn't entirely foolproof: in case there's an invalid line with something unexpected and negative numbers, we're probably going to get a ValueError from the first conversion of val1/val2 to float. It might also break when subtracting a number from a negative number, or similar corner cases. Further safeguards (such as catching exceptions coming from float()) could be added to the code; my point is to prove that you can very easily use a safe and controlled approach instead of eval.

An example run with your data (hat tip again to PM 2Ring) plus an intentionally invalid input line:

data = '''\
2 + 2
3 * 3
4.5 - 2.3
-8.8 + 1.2
2 ^ 4
8.9 / 2.3
5 % 3
-2 * -2
3 @ 4
'''.splitlines()

for line in data:
    print(evaluate(line))

The corresponding output in an interactive shell:

4
9
2.2
-7.6000000000000005
16
3.8695652173913047
2
4
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "foo.py", line 26, in evaluate
    raise ValueError('Invalid input line "{}"!'.format(expr))
ValueError: Invalid input line "3 @ 4"!