web-dev-qa-db-fra.com

Évaluation d'une expression mathématique dans une chaîne

stringExp = "2^4"
intVal = int(stringExp)      # Expected value: 16

Cela renvoie l'erreur suivante:

Traceback (most recent call last):  
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int()
with base 10: '2^4'

Je sais que eval peut contourner ce problème, mais n’existe-t-il pas une méthode plus sûre et plus importante pour évaluer une expression mathématique stockée dans une chaîne?

88
Pieter

Pyparsing peut être utilisé pour analyser des expressions mathématiques. En particulier, fourFn.py .__ montre comment analyser des expressions arithmétiques de base. Ci-dessous, j'ai réemballé fourFn dans une classe d'analyseur numérique pour une réutilisation plus facile. 

from __future__ import division
from pyparsing import (Literal, CaselessLiteral, Word, Combine, Group, Optional,
                       ZeroOrMore, Forward, nums, alphas, oneOf)
import math
import operator

__author__ = 'Paul McGuire'
__version__ = '$Revision: 0.0 $'
__date__ = '$Date: 2009-03-20 $'
__source__ = '''http://pyparsing.wikispaces.com/file/view/fourFn.py
http://pyparsing.wikispaces.com/message/view/home/15549426
'''
__note__ = '''
All I've done is rewrap Paul McGuire's fourFn.py as a class, so I can use it
more easily in other places.
'''


class NumericStringParser(object):
    '''
    Most of this code comes from the fourFn.py pyparsing example

    '''

    def pushFirst(self, strg, loc, toks):
        self.exprStack.append(toks[0])

    def pushUMinus(self, strg, loc, toks):
        if toks and toks[0] == '-':
            self.exprStack.append('unary -')

    def __init__(self):
        """
        expop   :: '^'
        multop  :: '*' | '/'
        addop   :: '+' | '-'
        integer :: ['+' | '-'] '0'..'9'+
        atom    :: PI | E | real | fn '(' expr ')' | '(' expr ')'
        factor  :: atom [ expop factor ]*
        term    :: factor [ multop factor ]*
        expr    :: term [ addop term ]*
        """
        point = Literal(".")
        e = CaselessLiteral("E")
        fnumber = Combine(Word("+-" + nums, nums) +
                          Optional(point + Optional(Word(nums))) +
                          Optional(e + Word("+-" + nums, nums)))
        ident = Word(alphas, alphas + nums + "_$")
        plus = Literal("+")
        minus = Literal("-")
        mult = Literal("*")
        div = Literal("/")
        lpar = Literal("(").suppress()
        rpar = Literal(")").suppress()
        addop = plus | minus
        multop = mult | div
        expop = Literal("^")
        pi = CaselessLiteral("PI")
        expr = Forward()
        atom = ((Optional(oneOf("- +")) +
                 (ident + lpar + expr + rpar | pi | e | fnumber).setParseAction(self.pushFirst))
                | Optional(oneOf("- +")) + Group(lpar + expr + rpar)
                ).setParseAction(self.pushUMinus)
        # by defining exponentiation as "atom [ ^ factor ]..." instead of
        # "atom [ ^ atom ]...", we get right-to-left exponents, instead of left-to-right
        # that is, 2^3^2 = 2^(3^2), not (2^3)^2.
        factor = Forward()
        factor << atom + \
            ZeroOrMore((expop + factor).setParseAction(self.pushFirst))
        term = factor + \
            ZeroOrMore((multop + factor).setParseAction(self.pushFirst))
        expr << term + \
            ZeroOrMore((addop + term).setParseAction(self.pushFirst))
        # addop_term = ( addop + term ).setParseAction( self.pushFirst )
        # general_term = term + ZeroOrMore( addop_term ) | OneOrMore( addop_term)
        # expr <<  general_term
        self.bnf = expr
        # map operator symbols to corresponding arithmetic operations
        epsilon = 1e-12
        self.opn = {"+": operator.add,
                    "-": operator.sub,
                    "*": operator.mul,
                    "/": operator.truediv,
                    "^": operator.pow}
        self.fn = {"sin": math.sin,
                   "cos": math.cos,
                   "tan": math.tan,
                   "exp": math.exp,
                   "abs": abs,
                   "trunc": lambda a: int(a),
                   "round": round,
                   "sgn": lambda a: abs(a) > epsilon and cmp(a, 0) or 0}

    def evaluateStack(self, s):
        op = s.pop()
        if op == 'unary -':
            return -self.evaluateStack(s)
        if op in "+-*/^":
            op2 = self.evaluateStack(s)
            op1 = self.evaluateStack(s)
            return self.opn[op](op1, op2)
        Elif op == "PI":
            return math.pi  # 3.1415926535
        Elif op == "E":
            return math.e  # 2.718281828
        Elif op in self.fn:
            return self.fn[op](self.evaluateStack(s))
        Elif op[0].isalpha():
            return 0
        else:
            return float(op)

    def eval(self, num_string, parseAll=True):
        self.exprStack = []
        results = self.bnf.parseString(num_string, parseAll)
        val = self.evaluateStack(self.exprStack[:])
        return val

Vous pouvez l'utiliser comme ça

nsp = NumericStringParser()
result = nsp.eval('2^4')
print(result)
# 16.0

result = nsp.eval('exp(2^4)')
print(result)
# 8886110.520507872
87
unutbu

eval est diabolique

eval("__import__('os').remove('important file')") # arbitrary commands
eval("9**9**9**9**9**9**9**9", {'__builtins__': None}) # CPU, memory

Remarque: même si vous utilisez set __builtins__ sur None, il est toujours possible d'échapper à l'introspection:

eval('(1).__class__.__bases__[0].__subclasses__()', {'__builtins__': None})

Évaluez l'expression arithmétique en utilisant ast

import ast
import operator as op

# supported operators
operators = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,
             ast.Div: op.truediv, ast.Pow: op.pow, ast.BitXor: op.xor,
             ast.USub: op.neg}

def eval_expr(expr):
    """
    >>> eval_expr('2^6')
    4
    >>> eval_expr('2**6')
    64
    >>> eval_expr('1 + 2*3**(4^5) / (6 + -7)')
    -5.0
    """
    return eval_(ast.parse(expr, mode='eval').body)

def eval_(node):
    if isinstance(node, ast.Num): # <number>
        return node.n
    Elif isinstance(node, ast.BinOp): # <left> <operator> <right>
        return operators[type(node.op)](eval_(node.left), eval_(node.right))
    Elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1
        return operators[type(node.op)](eval_(node.operand))
    else:
        raise TypeError(node)

Vous pouvez facilement limiter la plage autorisée pour chaque opération ou tout résultat intermédiaire, par exemple, pour limiter les arguments d'entrée pour a**b:

def power(a, b):
    if any(abs(n) > 100 for n in [a, b]):
        raise ValueError((a,b))
    return op.pow(a, b)
operators[ast.Pow] = power

Ou pour limiter la magnitude des résultats intermédiaires:

import functools

def limit(max_=None):
    """Return decorator that limits allowed returned values."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            ret = func(*args, **kwargs)
            try:
                mag = abs(ret)
            except TypeError:
                pass # not applicable
            else:
                if mag > max_:
                    raise ValueError(ret)
            return ret
        return wrapper
    return decorator

eval_ = limit(max_=10**100)(eval_)

Exemple

>>> evil = "__import__('os').remove('important file')"
>>> eval_expr(evil) #doctest:+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError:
>>> eval_expr("9**9")
387420489
>>> eval_expr("9**9**9**9**9**9**9**9") #doctest:+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValueError:
155
jfs

Quelques alternatives plus sûres que eval() et sympy.sympify().evalf()*:

*SymPy sympify est également dangereux, selon l’avertissement suivant de la documentation.

Attention: Notez que cette fonction utilise eval et ne devrait donc pas être utilisée sur une entrée non authentifiée.

11
Mark Mikofski

Le problème avec eval, c’est qu’il peut échapper trop facilement à son bac à sable, même si vous vous débarrassez de __builtins__. Toutes les méthodes pour sortir du sandbox consistent à utiliser getattr ou object.__getattribute__ (via l'opérateur .) pour obtenir une référence à un objet dangereux via un objet autorisé (''.__class__.__bases__[0].__subclasses__ ou similaire). getattr est éliminé en définissant __builtins__ sur None. object.__getattribute__ est difficile, car il ne peut pas être simplement supprimé, à la fois parce que object est immuable et parce que le supprimer casserait tout. Cependant, __getattribute__ n’est accessible que par l’opérateur .; il est donc suffisant de purger cette information de votre entrée pour éviter à eval de sortir de son sandbox.
Dans le traitement des formules, la seule utilisation valide d’une décimale est qu’elle soit précédée ou suivie de [0-9]; nous supprimons donc toutes les autres instances de ..

import re
inp = re.sub(r"\.(?![0-9])","", inp)
val = eval(inp, {'__builtins__':None})

Notez que bien que python traite normalement 1 + 1. en tant que 1 + 1.0, cela supprimera le . final et vous laissera avec 1 + 1. Vous pouvez ajouter ), et EOF à la liste des choses autorisées à suivre ., mais pourquoi s'embêter?

7
Perkins

La raison pour laquelle eval et exec sont si dangereux est que la fonction compile par défaut générera du bytecode pour toute expression python valide et que la valeur par défaut eval ou exec exécutera tout bytecode python valide. À ce jour, toutes les réponses se sont concentrées sur la limitation du code-octet pouvant être généré (en supprimant les entrées) ou sur la construction de votre propre langage spécifique au domaine à l'aide de l'AST. 

Au lieu de cela, vous pouvez facilement créer une simple fonction eval qui est incapable de faire quoi que ce soit de néfaste et qui peut facilement avoir des vérifications d’exécution sur la mémoire ou le temps utilisé. Bien sûr, s'il s'agit d'un calcul simple, il existe un raccourci.

c = compile(stringExp, 'userinput', 'eval')
if c.co_code[0]==b'd' and c.co_code[3]==b'S':
    return c.co_consts[ord(c.co_code[1])+ord(c.co_code[2])*256]

La façon dont cela fonctionne est simple, toute expression mathématique constante est évaluée en toute sécurité lors de la compilation et stockée sous forme de constante. L'objet de code renvoyé par compile est constitué de d, qui est le bytecode de LOAD_CONST, suivi du numéro de la constante à charger (généralement la dernière dans la liste), suivi de S, qui est le bytecode de RETURN_VALUE. Si ce raccourci ne fonctionne pas, cela signifie que la saisie de l'utilisateur n'est pas une expression constante (contient une variable, un appel à une fonction ou similaire). 

Cela ouvre également la porte à des formats d’entrée plus sophistiqués. Par exemple:

stringExp = "1 + cos(2)"

Cela nécessite en fait d’évaluer le bytecode, qui reste assez simple. Le bytecode Python est un langage orienté pile, donc tout est une simple question de TOS=stack.pop(); op(TOS); stack.put(TOS) ou similaire. La clé consiste à implémenter uniquement les opcodes sécurisés (chargement/stockage de valeurs, opérations mathématiques, valeurs renvoyées) et non insalubres (recherche d'attributs). Si vous voulez que l’utilisateur puisse appeler des fonctions (toute la raison pour ne pas utiliser le raccourci ci-dessus), il suffit de faire en sorte que votre implémentation de CALL_FUNCTION n’autorise que les fonctions figurant dans une liste "sans danger".

from dis import opmap
from Queue import LifoQueue
from math import sin,cos
import operator

globs = {'sin':sin, 'cos':cos}
safe = globs.values()

stack = LifoQueue()

class BINARY(object):
    def __init__(self, operator):
        self.op=operator
    def __call__(self, context):
        stack.put(self.op(stack.get(),stack.get()))

class UNARY(object):
    def __init__(self, operator):
        self.op=operator
    def __call__(self, context):
        stack.put(self.op(stack.get()))


def CALL_FUNCTION(context, arg):
    argc = arg[0]+arg[1]*256
    args = [stack.get() for i in range(argc)]
    func = stack.get()
    if func not in safe:
        raise TypeError("Function %r now allowed"%func)
    stack.put(func(*args))

def LOAD_CONST(context, arg):
    cons = arg[0]+arg[1]*256
    stack.put(context['code'].co_consts[cons])

def LOAD_NAME(context, arg):
    name_num = arg[0]+arg[1]*256
    name = context['code'].co_names[name_num]
    if name in context['locals']:
        stack.put(context['locals'][name])
    else:
        stack.put(context['globals'][name])

def RETURN_VALUE(context):
    return stack.get()

opfuncs = {
    opmap['BINARY_ADD']: BINARY(operator.add),
    opmap['UNARY_INVERT']: UNARY(operator.invert),
    opmap['CALL_FUNCTION']: CALL_FUNCTION,
    opmap['LOAD_CONST']: LOAD_CONST,
    opmap['LOAD_NAME']: LOAD_NAME
    opmap['RETURN_VALUE']: RETURN_VALUE,
}

def VMeval(c):
    context = dict(locals={}, globals=globs, code=c)
    bci = iter(c.co_code)
    for bytecode in bci:
        func = opfuncs[ord(bytecode)]
        if func.func_code.co_argcount==1:
            ret = func(context)
        else:
            args = ord(bci.next()), ord(bci.next())
            ret = func(context, args)
        if ret:
            return ret

def evaluate(expr):
    return VMeval(compile(expr, 'userinput', 'eval'))

De toute évidence, la version réelle serait un peu plus longue (il y a 119 opcodes, dont 24 sont liés aux mathématiques). Ajouter STORE_FAST et quelques autres permettrait une entrée comme 'x=5;return x+x ou similaire, de manière triviale. Il peut même être utilisé pour exécuter des fonctions créées par l'utilisateur, à condition que les fonctions créées par l'utilisateur soient elles-mêmes exécutées via VMeval (ne les rendez pas appelables !!! sinon elles pourraient être utilisées comme un rappel quelque part). La gestion des boucles nécessite la prise en charge des bytecodes goto, ce qui signifie que vous passez d'un itérateur for à while et que vous maintenez un pointeur sur l'instruction en cours, mais ce n'est pas trop difficile. Pour la résistance à la DOS, la boucle principale doit vérifier le temps écoulé depuis le début du calcul et certains opérateurs doivent refuser les entrées dépassant une limite raisonnable (BINARY_POWER étant le plus évident).

Bien que cette approche soit un peu plus longue qu'un simple analyseur de grammaire pour les expressions simples (voir ci-dessus à propos de la saisie de la constante compilée), elle s'étend facilement à une saisie plus complexe et ne nécessite pas de traitement de la grammaire (compile prend tout ce qui est arbitrairement compliqué et le réduit à une séquence d'instructions simples).

6
Perkins

Vous pouvez utiliser le module ast et écrire un NodeVisitor qui vérifie que le type de chaque nœud fait partie d'une liste blanche.

import ast, math

locals =  {key: value for (key,value) in vars(math).items() if key[0] != '_'}
locals.update({"abs": abs, "complex": complex, "min": min, "max": max, "pow": pow, "round": round})

class Visitor(ast.NodeVisitor):
    def visit(self, node):
       if not isinstance(node, self.whitelist):
           raise ValueError(node)
       return super().visit(node)

    whitelist = (ast.Module, ast.Expr, ast.Load, ast.Expression, ast.Add, ast.Sub, ast.UnaryOp, ast.Num, ast.BinOp,
            ast.Mult, ast.Div, ast.Pow, ast.BitOr, ast.BitAnd, ast.BitXor, ast.USub, ast.UAdd, ast.FloorDiv, ast.Mod,
            ast.LShift, ast.RShift, ast.Invert, ast.Call, ast.Name)

def evaluate(expr, locals = {}):
    if any(elem in expr for elem in '\n#') : raise ValueError(expr)
    try:
        node = ast.parse(expr.strip(), mode='eval')
        Visitor().visit(node)
        return eval(compile(node, "<string>", "eval"), {'__builtins__': None}, locals)
    except Exception: raise ValueError(expr)

Parce que cela fonctionne via une liste blanche plutôt que sur une liste noire, il est sûr. Les seules fonctions et variables auxquelles il peut accéder sont celles auxquelles vous lui donnez explicitement accès. J'ai rempli un dict avec des fonctions liées aux mathématiques afin que vous puissiez facilement y accéder si vous le souhaitez, mais vous devez l'utiliser explicitement.

Si la chaîne tente d'appeler des fonctions qui n'ont pas été fournies ou d'appeler des méthodes, une exception sera levée et elle ne sera pas exécutée.

Comme il utilise l’analyseur et l’évaluateur intégrés de Python, il hérite également des règles de priorité et de promotion de Python.

>>> evaluate("7 + 9 * (2 << 2)")
79
>>> evaluate("6 // 2 + 0.0")
3.0

Le code ci-dessus n'a été testé que sur Python 3.

Si vous le souhaitez, vous pouvez ajouter un décorateur de délai d’exécution pour cette fonction.

5
Kevin

C'est une réponse massivement en retard, mais je pense utile pour référence future. Plutôt que d'écrire votre propre analyseur mathématique (bien que l'exemple ci-dessus soit génial), vous pouvez utiliser SymPy. Je n'ai pas beaucoup d'expérience avec cela, mais il contient un moteur de calcul beaucoup plus puissant que quiconque est susceptible d'écrire pour une application spécifique et l'évaluation de l'expression de base est très simple:

>>> import sympy
>>> x, y, z = sympy.symbols('x y z')
>>> sympy.sympify("x**3 + sin(y)").evalf(subs={x:1, y:-3})
0.858879991940133

Très cool en effet! Un from sympy import * apporte beaucoup plus de fonctionnalités, telles que des fonctions trigonométriques, des fonctions spéciales, etc., mais je l'ai évité ici pour montrer ce qui vient de où.

5
andybuckley

_ {[Je sais que c'est une vieille question, mais il convient de signaler de nouvelles solutions utiles à mesure qu'elles apparaissent]]

Depuis python3.6, cette capacité est maintenant intégrée dans le langage, inventé "f-strings".

Voir: PEP 498 - Interpolation de chaîne littérale

Par exemple (notez le préfixe f):

f'{2**4}'
=> '16'
3
shx2

Je pense que j'utiliserais eval(), mais vérifierais d'abord que la chaîne est une expression mathématique valide, par opposition à quelque chose de malveillant. Vous pouvez utiliser un regex pour la validation.

eval() accepte également des arguments supplémentaires que vous pouvez utiliser pour restreindre l'espace de noms dans lequel il opère, pour une sécurité accrue.

3
Tim Goodman

Si vous ne souhaitez pas utiliser eval, la seule solution consiste à implémenter l'analyseur de grammaire approprié. Regardez pyparsing .

1
kgiannakakis

Utilisez eval dans un espace de noms propre:

>>> ns = {'__builtins__': None}
>>> eval('2 ** 4', ns)
16

L'espace de noms propre devrait empêcher l'injection. Par exemple:

>>> eval('__builtins__.__import__("os").system("echo got through")', ns)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute '__import__'

Sinon, vous obtiendriez:

>>> eval('__builtins__.__import__("os").system("echo got through")')
got through
0

Vous voudrez peut-être donner accès au module mathématique:

>>> import math
>>> ns = vars(math).copy()
>>> ns['__builtins__'] = None
>>> eval('cos(pi/3)', ns)
0.50000000000000011
0
krawyoti

Si vous utilisez déjà wolframalpha, ils ont une API python qui vous permet d’évaluer des expressions. Peut-être un peu lent, mais au moins très précis. 

https://pypi.python.org/pypi/wolframalpha

0
user1767754

Voici ma solution au problème sans utiliser eval. Fonctionne avec Python2 et Python3. Cela ne fonctionne pas avec des nombres négatifs.

$ python -m pytest test.py

test.py

from solution import Solutions

class SolutionsTestCase(unittest.TestCase):
    def setUp(self):
        self.solutions = Solutions()

    def test_evaluate(self):
        expressions = [
            '2+3=5',
            '6+4/2*2=10',
            '3+2.45/8=3.30625',
            '3**3*3/3+3=30',
            '2^4=6'
        ]
        results = [x.split('=')[1] for x in expressions]
        for e in range(len(expressions)):
            if '.' in results[e]:
                results[e] = float(results[e])
            else:
                results[e] = int(results[e])
            self.assertEqual(
                results[e],
                self.solutions.evaluate(expressions[e])
            )

solution.py

class Solutions(object):
    def evaluate(self, exp):
        def format(res):
            if '.' in res:
                try:
                    res = float(res)
                except ValueError:
                    pass
            else:
                try:
                    res = int(res)
                except ValueError:
                    pass
            return res
        def splitter(item, op):
            mul = item.split(op)
            if len(mul) == 2:
                for x in ['^', '*', '/', '+', '-']:
                    if x in mul[0]:
                        mul = [mul[0].split(x)[1], mul[1]]
                    if x in mul[1]:
                        mul = [mul[0], mul[1].split(x)[0]]
            Elif len(mul) > 2:
                pass
            else:
                pass
            for x in range(len(mul)):
                mul[x] = format(mul[x])
            return mul
        exp = exp.replace(' ', '')
        if '=' in exp:
            res = exp.split('=')[1]
            res = format(res)
            exp = exp.replace('=%s' % res, '')
        while '^' in exp:
            if '^' in exp:
                itm = splitter(exp, '^')
                res = itm[0] ^ itm[1]
                exp = exp.replace('%s^%s' % (str(itm[0]), str(itm[1])), str(res))
        while '**' in exp:
            if '**' in exp:
                itm = splitter(exp, '**')
                res = itm[0] ** itm[1]
                exp = exp.replace('%s**%s' % (str(itm[0]), str(itm[1])), str(res))
        while '/' in exp:
            if '/' in exp:
                itm = splitter(exp, '/')
                res = itm[0] / itm[1]
                exp = exp.replace('%s/%s' % (str(itm[0]), str(itm[1])), str(res))
        while '*' in exp:
            if '*' in exp:
                itm = splitter(exp, '*')
                res = itm[0] * itm[1]
                exp = exp.replace('%s*%s' % (str(itm[0]), str(itm[1])), str(res))
        while '+' in exp:
            if '+' in exp:
                itm = splitter(exp, '+')
                res = itm[0] + itm[1]
                exp = exp.replace('%s+%s' % (str(itm[0]), str(itm[1])), str(res))
        while '-' in exp:
            if '-' in exp:
                itm = splitter(exp, '-')
                res = itm[0] - itm[1]
                exp = exp.replace('%s-%s' % (str(itm[0]), str(itm[1])), str(res))

        return format(exp)
0
ART GALLERY