简体   繁体   中英

How to convert float to Decimal while using eval?

I'm new to programming so I thought I'd ask here for help. So when I use:

eval('12.5 + 3.2'),

it converts 12.5 and 3.2 into floats. But I want them to be converted into the Decimal datatype.

I can use:

from decimal import Decimal
eval(Decimal(12.5) + Decimal(3.2))

But I can't do that in my program as I'm accepting user input.

I've found a solution but it uses regular expressions, which I'm not familiar with right now (and I can't find it again for some reason).

It would be great if someone could help me out. Thanks!

UPDATE: apparently the official docs has a recipe that does exactly what you're looking for. From https://docs.python.org/3/library/tokenize.html#examples :

from tokenize import tokenize, untokenize, NUMBER, STRING, NAME, OP
from io import BytesIO

def decistmt(s):
    """Substitute Decimals for floats in a string of statements.

    >>> from decimal import Decimal
    >>> s = 'print(+21.3e-5*-.1234/81.7)'
    >>> decistmt(s)
    "print (+Decimal ('21.3e-5')*-Decimal ('.1234')/Decimal ('81.7'))"

    The format of the exponent is inherited from the platform C library.
    Known cases are "e-007" (Windows) and "e-07" (not Windows).  Since
    we're only showing 12 digits, and the 13th isn't close to 5, the
    rest of the output should be platform-independent.

    >>> exec(s)  #doctest: +ELLIPSIS
    -3.21716034272e-0...7

    Output from calculations with Decimal should be identical across all
    platforms.

    >>> exec(decistmt(s))
    -3.217160342717258261933904529E-7
    """
    result = []
    g = tokenize(BytesIO(s.encode('utf-8')).readline)  # tokenize the string
    for toknum, tokval, _, _, _ in g:
        if toknum == NUMBER and '.' in tokval:  # replace NUMBER tokens
            result.extend([
                (NAME, 'Decimal'),
                (OP, '('),
                (STRING, repr(tokval)),
                (OP, ')')
            ])
        else:
            result.append((toknum, tokval))
    return untokenize(result).decode('utf-8')

Which you can then use like so:

from decimal import Decimal
s = "12.5 + 3.2 + 1.0000000000000001 + (1.0 if 2.0 else 3.0)"
s = decistmt(s)
print(s)
print(eval(s))

Result:

Decimal ('12.5')+Decimal ('3.2')+Decimal ('1.0000000000000001')+(Decimal ('1.0')if Decimal ('2.0')else Decimal ('3.0'))
17.7000000000000001

Feel free to skip the rest of this answer, which is now only of interest to historians of half-correct solutions.


As far as I know, there's no easy way to "hook into" eval in order to change how it interprets float objects.

But if we use the ast module to convert your string into an abstract syntax tree before eval ing it, then we can manipulate the tree to replace the floats with Decimal calls.

import ast
from decimal import Decimal

def construct_decimal_node(value):
    return ast.Call(
        func = ast.Name(id="Decimal", ctx=ast.Load()),
        args = [value],
        keywords = []
    )
    return expr

class FloatLiteralReplacer(ast.NodeTransformer):
    def visit_Num(self, node):
        return construct_decimal_node(node)

s = '12.5 + 3.2'
node = ast.parse(s, mode="eval")
node = FloatLiteralReplacer().visit(node)
ast.fix_missing_locations(node) #add diagnostic information to the nodes we created
code = compile(node, filename="", mode="eval")
result = eval(code)
print("The type of the result of this expression is:", type(result))
print("The result of this expression is:", result)

Result:

The type of the result of this expression is: <class 'decimal.Decimal'>
The result of this expression is: 15.70000000000000017763568394

As you can see, the result is identical to what you would have gotten if you had calculated Decimal(12.5) + Decimal(3.2) directly.


But perhaps you're thinking "Why isn't the result 15.7?". This is because Decimal(3.2) is not exactly identical to 3.2. It's actually equal to 3.20000000000000017763568394002504646778106689453125 . This is a hazard when it comes to initializing decimals using float objects -- the inaccuracy is already present. Better to use strings to create decimals, eg Decimal("3.2") .

Maybe you're now thinking "Ok, so how do I turn 12.5 + 3.2 into Decimal("12.5") + Decimal("3.2") ?". The quickest approach would be to modify construct_decimal_node so the Call's args is an ast.Str rather than an ast.Num:

import ast
from decimal import Decimal

def construct_decimal_node(value):
    return ast.Call(
        func = ast.Name(id="Decimal", ctx=ast.Load()),
        args = [ast.Str(str(value.n))],
        keywords = []
    )
    return expr

class FloatLiteralReplacer(ast.NodeTransformer):
    def visit_Num(self, node):
        return construct_decimal_node(node)

s = '12.5 + 3.2'
node = ast.parse(s, mode="eval")
node = FloatLiteralReplacer().visit(node)
ast.fix_missing_locations(node) #add diagnostic information to the nodes we created
code = compile(node, filename="", mode="eval")
result = eval(code)
print("The type of the result of this expression is:", type(result))
print("The result of this expression is:", result)

Result:

The type of the result of this expression is: <class 'decimal.Decimal'>
The result of this expression is: 15.7

But take care: while I expect this approach to return good results most of the time, there is a corner case where it returns surprising results. In particular, when the expression contains a float f such that float(str(f)) != f . In other words, when the printed representation of the float lacks the precision necessary to represent the float exactly.

For example, if you changed s in the above code to "1.0000000000000001 + 0" , the result would be 1.0 . This is incorrect, since the result of Decimal("1.0000000000000001") + Decimal("0") is 1.0000000000000001 .

I'm not sure how you could prevent this problem... By the time ast.parse has finished executing, the float literal has already been converted into a float object, and there's no obvious way to retrieve the string that was used to create it. Perhaps you could extract it from the expression string, but you'd basically have to reinvent Python's parser to do that.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM