it-swarm.com.de

Auswerten eines mathematischen Ausdrucks in einer Zeichenfolge

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

Dies gibt den folgenden Fehler zurück:

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

Ich weiß, dass eval dies umgehen kann, aber gibt es nicht eine bessere und vor allem sicherere Methode, um einen mathematischen Ausdruck zu bewerten, der in einem String gespeichert wird?

88
Pieter

Pyparsing kann verwendet werden, um mathematische Ausdrücke zu analysieren. Insbesondere zeigt fourFn.py , Wie grundlegende arithmetische Ausdrücke zu analysieren sind. Im Folgenden habe ich fourFn zur leichteren Wiederverwendung in eine numerische Parserklasse umgepackt. 

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

Sie können es so verwenden

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

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

eval ist böse

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

Hinweis: Selbst wenn Sie __builtins__ bis None verwenden, kann es immer noch möglich sein, die Introspektion aufzubrechen:

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

Berechnen Sie den arithmetischen Ausdruck mit 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)

Sie können den zulässigen Bereich für jede Operation oder jedes Zwischenergebnis problemlos einschränken, z. B. um Eingabeargumente für a**b zu begrenzen:

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

Oder um die Größenordnung der Zwischenergebnisse zu begrenzen:

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_)

Beispiel

>>> 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

Einige sicherere Alternativen zu eval() und sympy.sympify().evalf()*:

*SymPy sympify ist auch gemäß der folgenden Warnung aus der Dokumentation unsicher.

Warning: Beachten Sie, dass diese Funktion eval verwendet und daher nicht für nicht gesicherte Eingaben verwendet werden sollte.

11
Mark Mikofski

Okay, das Problem mit eval ist, dass es zu leicht aus seiner Sandbox entkommen kann, selbst wenn Sie __builtins__ loswerden. Für alle Methoden zur Flucht vor der Sandbox wird getattr oder object.__getattribute__ (über den .-Operator) verwendet, um über ein zulässiges Objekt (''.__class__.__bases__[0].__subclasses__ oder ähnliches) einen Verweis auf ein gefährliches Objekt zu erhalten. getattr wird durch Setzen von __builtins__ auf None gelöscht. object.__getattribute__ ist der schwierigste, da er nicht einfach entfernt werden kann, weil object unveränderlich ist und das Entfernen alles zerstört. Auf __getattribute__ ist jedoch nur über den .-Operator zugegriffen, sodass das Löschen Ihrer Eingabe ausreichend ist, um sicherzustellen, dass eval nicht aus seiner Sandbox entkommen kann.
Bei der Verarbeitung von Formeln kann eine Dezimalzahl nur dann gültig sein, wenn sie [0-9] vorangestellt wird oder von ihr gefolgt wird. Daher entfernen wir einfach alle anderen Instanzen von ..

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

Beachten Sie, dass, während Python 1 + 1. normalerweise als 1 + 1.0 behandelt, der nachfolgende . entfernt wird und Sie mit 1 + 1 verlassen werden. Sie könnten ), und EOF zur Liste der Dinge hinzufügen, denen . folgen darf, aber warum sollten Sie sich die Mühe machen?

7
Perkins

Der Grund von eval und exec ist so gefährlich, dass die Standardfunktion compile Bytecode für jeden gültigen Python-Ausdruck generiert, und der Standardwert eval oder exec jeden gültigen Python-Bytecode ausführt. Alle bisherigen Antworten haben sich darauf konzentriert, den Bytecode, der generiert werden kann, zu beschränken (durch Bereinigen der Eingabe) oder das Erstellen einer eigenen domänenspezifischen Sprache mithilfe des AST. 

Stattdessen können Sie einfach eine einfache eval-Funktion erstellen, die nicht in der Lage ist, etwas Schändliches zu tun, und kann leicht Laufzeitprüfungen des Speichers oder der verwendeten Zeit durchführen. Wenn es sich um einfache Mathematik handelt, gibt es natürlich eine Verknüpfung.

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]

Die Funktionsweise ist einfach: Jeder konstante mathematische Ausdruck wird während der Kompilierung sicher ausgewertet und als Konstante gespeichert. Das von compile zurückgegebene Code-Objekt besteht aus d, das ist der Bytecode für LOAD_CONST, gefolgt von der Nummer der zu ladenden Konstante (normalerweise die letzte in der Liste), gefolgt von S, das ist der Bytecode für RETURN_VALUE. Wenn diese Verknüpfung nicht funktioniert, bedeutet dies, dass die Benutzereingabe kein konstanter Ausdruck ist (enthält einen Variablen- oder Funktionsaufruf oder ähnliches). 

Dies öffnet auch die Tür zu anspruchsvolleren Eingabeformaten. Zum Beispiel:

stringExp = "1 + cos(2)"

Dies erfordert das Auswerten des Bytecodes, was immer noch recht einfach ist. Python-Bytecode ist eine stapelorientierte Sprache, daher ist alles eine einfache Angelegenheit von TOS=stack.pop(); op(TOS); stack.put(TOS) oder ähnlich. Der Schlüssel ist, nur die Opcodes zu implementieren, die sicher sind (Laden/Speichern von Werten, mathematische Operationen, Rückgabe von Werten) und keine unsicheren (Attributsuche). Wenn Sie möchten, dass der Benutzer Funktionen aufrufen kann (der gesamte Grund, die oben genannte Verknüpfung nicht zu verwenden), können Sie einfach die Implementierung von CALL_FUNCTION nur Funktionen in einer "sicheren" Liste zulassen.

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'))

Offensichtlich wäre die echte Version etwas länger (es gibt 119 Opcodes, von denen 24 mathematisch verwandt sind). Das Hinzufügen von STORE_FAST und ein paar anderen würde eine Eingabe wie 'x=5;return x+x oder ähnliches ermöglichen, ganz einfach. Es kann sogar verwendet werden, um vom Benutzer erstellte Funktionen auszuführen, solange die vom Benutzer erstellten Funktionen selbst über VMeval ausgeführt werden (machen Sie sie nicht aufrufbar !!! oder könnten sie irgendwo als Rückruf verwendet werden). Für die Verarbeitung von Schleifen ist die Unterstützung der goto-Bytecodes erforderlich. Dies bedeutet, dass Sie von einem for-Iterator zu while wechseln und einen Zeiger auf die aktuelle Anweisung beibehalten, aber nicht zu schwer. Für die Beständigkeit gegen DOS sollte die Hauptschleife überprüfen, wie viel Zeit seit dem Start der Berechnung vergangen ist, und bestimmte Operatoren sollten die Eingabe über eine vernünftige Grenze hinweg ablehnen (BINARY_POWER ist der offensichtlichste).

Dieser Ansatz ist zwar etwas länger als ein einfacher Grammatik-Parser für einfache Ausdrücke (siehe oben zum Kompilieren der kompilierten Konstante), er reicht aber auch für kompliziertere Eingaben aus und erfordert nicht den Umgang mit der Grammatik (compile nimmt etwas willkürlich komplizierte und reduziert sie eine Folge von einfachen Anweisungen).

6
Perkins

Sie können das ast-Modul verwenden und einen NodeVisitor schreiben, der überprüft, ob der Typ jedes Knotens Teil einer Whitelist ist.

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)

Da es über eine Whitelist anstatt über eine Blacklist funktioniert, ist es sicher. Die einzigen Funktionen und Variablen, auf die er zugreifen kann, sind die, auf die Sie explizit Zugriff haben. Ich habe ein Diktier mit mathematikbezogenen Funktionen aufgefüllt, so dass Sie den Zugriff auf diese einfach gewähren können, wenn Sie möchten, aber Sie müssen es explizit verwenden.

Wenn die Zeichenfolge versucht, Funktionen aufzurufen, die nicht bereitgestellt wurden, oder Methoden aufzurufen, wird eine Ausnahme ausgelöst und nicht ausgeführt.

Da hierbei der in Python integrierte Parser und Evaluator verwendet wird, erbt er auch die Prioritäts- und Beförderungsregeln von Python.

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

Der obige Code wurde nur mit Python 3 getestet.

Falls gewünscht, können Sie zu dieser Funktion einen Timeout-Dekorator hinzufügen.

5
Kevin

Dies ist eine äußerst späte Antwort, aber ich denke, es ist nützlich für die zukünftige Bezugnahme. Anstatt Ihren eigenen Mathematik-Parser zu schreiben (obwohl das Pyparsing-Beispiel oben großartig ist), können Sie SymPy verwenden. Ich habe nicht viel Erfahrung damit, aber es enthält eine viel leistungsfähigere mathematische Engine, als wahrscheinlich jeder für eine bestimmte Anwendung schreibt. Die grundlegende Auswertung von Ausdrücken ist sehr einfach:

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

Sehr cool! Ein from sympy import * bietet viel mehr Funktionsunterstützung wie Triggerfunktionen, Sonderfunktionen usw., aber ich habe dies hier vermieden, um zu zeigen, was von woher kommt.

5
andybuckley

[Ich weiß, dass dies eine alte Frage ist, aber es lohnt sich, auf neue nützliche Lösungen hinzuweisen, wenn sie auftauchen.)

Seit Python3.6 ist diese Funktion nun in die Sprache eingebaut, "f-strings" geprägt.

See: PEP 498 - Interpolation von Zeichenketten

Zum Beispiel (beachten Sie das Präfix f):

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

Ich denke, ich würde eval() verwenden, würde aber zuerst prüfen, ob der String ein gültiger mathematischer Ausdruck ist, im Gegensatz zu etwas Bösartigem. Sie könnten einen Regex für die Validierung verwenden.

eval() benötigt auch zusätzliche Argumente, mit denen Sie den verwendeten Namespace einschränken können, um die Sicherheit zu erhöhen.

3
Tim Goodman

Wenn Sie eval nicht verwenden möchten, besteht die einzige Lösung darin, den entsprechenden Grammatikparser zu implementieren. Schauen Sie sich pyparsing an.

1
kgiannakakis

Verwenden Sie eval in einem sauberen Namespace:

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

Der saubere Namespace sollte die Injektion verhindern. Zum Beispiel:

>>> 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__'

Sonst würden Sie bekommen:

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

Möglicherweise möchten Sie Zugriff auf das Mathematikmodul gewähren:

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

Wenn Sie bereits Wolframalpha verwenden, haben sie eine Python-API, mit der Sie Ausdrücke bewerten können. Ist vielleicht etwas langsam, aber zumindest sehr genau. 

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

0
user1767754

Hier ist meine Lösung für das Problem, ohne eval zu verwenden. Funktioniert mit Python2 und Python3. Bei negativen Zahlen funktioniert das nicht.

$ 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