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

113
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 cela evalpeut contourner ce problème, mais n'y a-t-il pas une méthode meilleure et, plus important encore, plus sûre pour évaluer une expression mathématique stockée dans une chaîne?

Pieter
la source
6
^ est l'opérateur XOR. La valeur attendue est 6. Vous voulez probablement pow (2,4).
kgiannakakis
25
ou plus pythoniquement 2 ** 4
fortran
1
Si vous ne souhaitez pas utiliser eval, la seule solution est d'implémenter l'analyseur grammatical approprié. Jetez un œil à pyparsing .
kgiannakakis

Réponses:

108

Pyparsing peut être utilisé pour analyser des expressions mathématiques. En particulier, fourFn.py montre comment analyser les expressions arithmétiques de base. Ci-dessous, j'ai reconditionné 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
unutbu
la source
180

eval est mal

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__to, Noneil est toujours possible de sortir en utilisant l'introspection:

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

Évaluer l'expression arithmétique à l'aide de 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 l'ampleur 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:
jfs
la source
29
Message très cool, merci. J'ai repris ce concept et essayé de créer une bibliothèque qui devrait être facile à utiliser: github.com/danthedeckie/simpleeval
Daniel Fairhead
cela peut-il être étendu pour les fonctions de import math?
Hotschke
2
Notez que ce ast.parsen'est pas sûr. Par exemple ast.parse('()' * 1000000, '<string>', 'single')bloque l'interpréteur.
Antti Haapala
1
@AnttiHaapala bon exemple. Est-ce un bogue dans l'interpréteur Python? Quoi qu'il en soit, une grande entrée est gérée de manière triviale, par exemple en utilisant if len(expr) > 10000: raise ValueError.
jfs
1
@AnttiHaapala pourriez-vous fournir un exemple qui ne peut pas être corrigé à l'aide du len(expr)chèque? Ou votre point est qu'il y a des bogues dans l'implémentation de Python et qu'il est donc impossible d'écrire du code sûr en général?
jfs
13

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

* SymPy sympifyest également dangereux selon l'avertissement suivant de la documentation.

Attention: notez que cette fonction utilise eval, et ne doit donc pas être utilisée sur une entrée non désinfectée.

Mark Mikofski
la source
10

D'accord, le problème avec eval est qu'il peut échapper trop facilement à son bac à sable, même si vous vous en débarrassez __builtins__. Toutes les méthodes pour sortir du bac à sable se résument à utiliser getattrou 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). getattrest éliminé en définissant __builtins__sur None. object.__getattribute__est la plus difficile, car elle ne peut pas être simplement supprimée, à la fois parce qu'elle objectest immuable et parce que la supprimer briserait tout. Cependant, __getattribute__n'est accessible que via l' .opérateur, de sorte que la purge de votre entrée est suffisante pour garantir que eval ne puisse pas sortir de son bac à sable.
Dans le traitement des formules, la seule utilisation valide d'une décimale est lorsqu'elle est précédée ou suivie de[0-9], nous supprimons donc simplement 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.comme 1 + 1.0, cela supprimera la fin .et vous laissera avec 1 + 1. Vous pouvez ajouter ), et EOFà la liste des choses autorisées à suivre ., mais à quoi bon?

Perkins
la source
Une question connexe avec une discussion intéressante peut être trouvée ici .
djvg
3
Que l'argument concernant la suppression .soit correct ou non pour le moment, cela laisse un potentiel de vulnérabilités de sécurité si les futures versions de Python introduisent une nouvelle syntaxe permettant d'accéder d'une autre manière aux objets ou fonctions dangereux. Cette solution est déjà dangereuse en Python 3.6 en raison de f-chaînes, qui permettent l'attaque suivante: f"{eval('()' + chr(46) + '__class__')}". Une solution basée sur la liste blanche plutôt que sur la liste noire sera plus sûre, mais il vaut vraiment mieux résoudre ce problème sans aucun problème eval.
kaya3
C'est un excellent point sur les futures fonctionnalités du langage introduisant de nouveaux problèmes de sécurité.
Perkins
8

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 qu'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 déclenchée et elle ne sera pas exécutée.

Comme cela 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 temporisation sur cette fonction.

Kevin
la source
7

La raison evalet execsont si dangereuses est que la compilefonction par défaut générera du bytecode pour toute expression python valide, et la valeur par défaut evalou execexécutera tout bytecode python valide. Toutes les réponses à ce jour se sont concentrées sur la restriction du bytecode qui peut être généré (en nettoyant l'entrée) ou sur la création de votre propre langage spécifique au domaine à l'aide de l'AST.

Au lieu de cela, vous pouvez facilement créer une evalfonction simple qui est incapable de faire quoi que ce soit de néfaste et qui peut facilement avoir des contrôles d'exécution sur la mémoire ou le temps utilisé. Bien sûr, s'il s'agit de mathématiques simples, 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 code retourné par compile se compose de d, qui est le bytecode pour LOAD_CONST, suivi du numéro de la constante à charger (généralement la dernière de la liste), suivi de S, qui est le bytecode pour RETURN_VALUE. Si ce raccourci ne fonctionne pas, cela signifie que l'entrée utilisateur n'est pas une expression constante (contient un appel de variable ou de fonction ou similaire).

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

stringExp = "1 + cos(2)"

Cela nécessite en fait d'évaluer le bytecode, ce qui est encore assez simple. Le bytecode Python est un langage orienté pile, donc tout est simple TOS=stack.pop(); op(TOS); stack.put(TOS)ou similaire. La clé est de n'implémenter que les opcodes qui sont sûrs (chargement / stockage de valeurs, opérations mathématiques, retour de valeurs) et non dangereux (recherche d'attributs). Si vous voulez que l'utilisateur puisse appeler des fonctions (la raison pour laquelle ne pas utiliser le raccourci ci-dessus), faites simplement votre implémentation de CALL_FUNCTIONn'autoriser que les fonctions dans une liste «sûre».

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 de cela serait un peu plus longue (il y a 119 opcodes, dont 24 sont liés aux mathématiques). L'ajout STORE_FASTet quelques autres permettraient des entrées similaires 'x=5;return x+xou similaires, très facilement. 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 !!! ou elles pourraient être utilisées comme un rappel quelque part). La gestion des boucles nécessite la prise en charge des gotobytecodes, ce qui signifie passer d'un foritérateur à whileet maintenir un pointeur vers l'instruction courante, mais ce n'est pas trop difficile. Pour la résistance au DOS, la boucle principale doit vérifier le temps écoulé depuis le début du calcul, et certains opérateurs doivent refuser l'entrée au-delà d'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 des expressions simples (voir ci-dessus à propos de la capture de la constante compilée), elle s'étend facilement à une entrée plus compliquée, et ne nécessite pas de traiter la grammaire ( compileprenez quelque chose de compliqué arbitrairement et le réduit à une séquence d'instructions simples).

Perkins
la source
6

Je pense que j'utiliserais eval(), mais je 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() prend également des arguments supplémentaires que vous pouvez utiliser pour restreindre l'espace de noms dans lequel il opère pour une plus grande sécurité.

Tim Goodman
la source
3
Mais, bien sûr, ne vous fiez pas aux expressions régulières pour valider des expressions mathématiques arbitraires.
High Performance Mark
@ High-Performance Mark: Oui, je suppose que cela dépend du type d'expressions mathématiques qu'il a en tête. . . par exemple, simplement arithmétique simple avec les chiffres et +, -, *, /, **, (, )ou quelque chose de plus compliqué
Tim Goodman
@Tim - c'est le () qui m'inquiète, ou plutôt le ((((())))))). En vérité, je pense qu'OP devrait s'inquiéter à leur sujet, mon front est défoncé par les problèmes d'OP.
High Performance Mark
2
Ne pas utiliser eval()si vous ne contrôlez pas l'entrée même si vous limitez l'espace de noms, par exemple, eval("9**9**9**9**9**9**9**9", {'__builtins__': None})consomme du processeur, de la mémoire.
jfs
3
La restriction de l'espace de noms d'eval n'ajoute pas à la sécurité .
Antti Haapala
5

C'est une réponse massivement tardive, mais je pense utile pour référence future. Plutôt que d'écrire votre propre analyseur mathématique (bien que l'exemple de pyparsing ci-dessus soit excellent), vous pouvez utiliser SymPy. Je n'ai pas beaucoup d'expérience avec cela, mais il contient un moteur mathématique beaucoup plus puissant que quiconque est susceptible d'écrire pour une application spécifique et l'évaluation des expressions 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! A from sympy import *apporte beaucoup plus de support de fonctions, telles que des fonctions trigonométriques, des fonctions spéciales, etc., mais j'ai évité cela ici pour montrer ce qui vient d'où.

Andybuckley
la source
3
Sympy est-il «sûr»? Il semble y avoir de nombreux articles suggérant qu'il s'agit d'un wrapper autour de eval () qui pourrait être exploité de la même manière. Aussi evalfne prend pas numpy ndarrays.
Mark Mikofski
14
Aucune sympy n'est pas sûre pour une entrée non fiable. Essayez sympy.sympify("""[].__class__.__base__.__subclasses__()[158]('ls')""")ces appels subprocess.Popen()que j'ai passés à la lsplace rm -rf /. L'index sera probablement différent sur d'autres ordinateurs. Ceci est une variante de l' exploit Ned Batchelder
Mark Mikofski
1
En effet, cela n'ajoute pas du tout à la sécurité.
Antti Haapala
4

[Je sais que c'est une vieille question, mais cela vaut la peine de signaler de nouvelles solutions utiles au fur et à mesure qu'elles apparaissent]

Depuis python3.6, cette capacité est désormais intégrée au langage , appelé "f-strings" .

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

Par exemple (notez le fpréfixe):

f'{2**4}'
=> '16'
shx2
la source
7
Lien très intéressant. Mais je suppose que les chaînes f sont là pour faciliter l'écriture du code source, alors que la question semble être de travailler avec des chaînes à l'intérieur de variables (peut-être provenant de sources non fiables). Les chaînes f ne peuvent pas être utilisées dans ce cas.
Bernhard
y a-t-il un moyen de faire quelque chose à l'effet de f '{2 {opérateur} 4}' où vous pouvez maintenant assigner à l'opérateur de faire 2 + 4 ou 2 * 4 ou 2-4 ou etc
Skyler
Cela équivaut pratiquement à faire str(eval(...)), donc ce n'est certainement pas plus sûr que eval.
kaya3
Semble être la même chose avec exec / eval ...
Victor VosMottor remercie Monica le
0

Utilisation evaldans un espace de noms propre:

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

L'espace de noms propre doit 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 obtiendrez:

>>> 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
krawyoti
la source
35
eval ("(1) .__ class __.__ bases __ [0] .__ sous-classes __ () [81] ('echo got through'.split ())", {' builtins ': None}) #escapes your sandbox
Perkins
6
Python 3.4: eval("""[i for i in (1).__class__.__bases__[0].__subclasses__() if i.__name__.endswith('BuiltinImporter')][0]().load_module('sys').modules['sys'].modules['os'].system('/bin/sh')""", {'__builtins__': None})exécute le bourne shell ...
Antti Haapala
8
Ce n'est pas sûr . Un code malveillant peut toujours être exécuté.
Paradoxe de Fermi
This is not safe- eh bien, je pense que c'est aussi sûr que d'utiliser bash en général. BTW: eval('math.sqrt(2.0)')<- "maths". est requis comme indiqué ci-dessus.
Hannu
0

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)
GALERIE D'ART
la source