Оценка математического выражения в строке - PullRequest
94 голосов
/ 03 марта 2010
stringExp = "2^4"
intVal = int(stringExp)      # Expected value: 16

Возвращает следующую ошибку:

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

Я знаю, что eval может обойти эту проблему, но нет ли лучшего и, что более важно, более безопасного метода для оценки математического выражения, хранящегося в строке?

Ответы [ 11 ]

163 голосов
/ 04 марта 2012

eval это зло

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

Примечание: даже если вы установите __builtins__ в None, все равно можно будет разобраться с помощью самоанализа:

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

Оценить арифметическое выражение, используя 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)

Вы можете легко ограничить допустимый диапазон для каждой операции или любого промежуточного результата, например, для ограничения входных аргументов для 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

Или для ограничения величины промежуточных результатов:

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

Пример * * тысяча двадцать-одна >>> 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:

91 голосов
/ 03 марта 2010

Pyparsing может использоваться для анализа математических выражений. В частности, fourFn.py показывает, как анализировать основные арифметические выражения. Ниже я перевернул fourFn в класс числового парсера для более легкого повторного использования.

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

Вы можете использовать это так

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

result = nsp.eval('exp(2^4)')
print(result)
# 8886110.520507872
11 голосов
/ 07 августа 2013

Некоторые более безопасные альтернативы eval() и sympy.sympify().evalf()*:

* SymPy sympify также небезопасно в соответствии со следующим предупреждением из документации.

Предупреждение: Обратите внимание, что эта функция использует eval, и, следовательно, не должна использоваться на неанизированном входе.

7 голосов
/ 22 августа 2014

Хорошо, проблема с eval в том, что он может слишком легко покинуть свою песочницу, даже если вы избавитесь от __builtins__. Все методы выхода из песочницы сводятся к использованию getattr или object.__getattribute__ (через оператор .) для получения ссылки на некоторый опасный объект через некоторый разрешенный объект (''.__class__.__bases__[0].__subclasses__ или аналогичный). getattr устраняется установкой __builtins__ на None. object.__getattribute__ - сложный вопрос, поскольку его нельзя просто удалить, потому что object является неизменным и потому, что его удаление разрушит все. Тем не менее, __getattribute__ доступен только через оператор ., поэтому очистка этого от вашего ввода достаточна для того, чтобы eval не смог покинуть свою песочницу.
При обработке формул единственное допустимое использование десятичного числа - это когда ему предшествует или следует [0-9], поэтому мы просто удаляем все другие экземпляры ..

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

Обратите внимание, что хотя python обычно обрабатывает 1 + 1. как 1 + 1.0, это удалит завершающий . и оставит вас с 1 + 1. Вы можете добавить ), и EOF в список вещей, которым разрешено следовать ., но зачем?

6 голосов
/ 11 марта 2016

Причина, по которой eval и exec настолько опасны, заключается в том, что функция compile по умолчанию генерирует байт-код для любого допустимого выражения python, а по умолчанию eval или exec выполняет любой допустимый байт-код python. Все ответы на сегодняшний день были сосредоточены на ограничении байт-кода, который может быть сгенерирован (путем очистки входных данных) или создании вашего собственного предметно-ориентированного языка с использованием AST.

Вместо этого вы можете легко создать простую функцию eval, которая не способна делать что-то гнусное и может легко проверять время выполнения на используемую память или время. Конечно, если это простая математика, то есть ярлык.

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]

Способ, которым это работает, прост: любое математическое выражение константы безопасно оценивается во время компиляции и сохраняется как константа. Кодовый объект, возвращаемый компиляцией, состоит из d, который является байт-кодом для LOAD_CONST, за которым следует номер загружаемой константы (обычно последней в списке), за которым следует S, который является байт-кодом для RETURN_VALUE. Если этот ярлык не работает, это означает, что пользовательский ввод не является константным выражением (содержит вызов переменной или функции или подобное).

Это также открывает двери для некоторых более сложных форматов ввода. Например:

stringExp = "1 + cos(2)"

Это требует фактической оценки байт-кода, что все еще довольно просто. Байт-код Python - это стек-ориентированный язык, поэтому все просто TOS=stack.pop(); op(TOS); stack.put(TOS) или подобное. Ключевым моментом является реализация только тех кодов операций, которые безопасны (загрузка / хранение значений, математические операции, возвращают значения) и не являются небезопасными (поиск атрибутов). Если вы хотите, чтобы пользователь мог вызывать функции (причина не использовать ярлык выше), просто сделайте вашу реализацию CALL_FUNCTION разрешающей функции только в «безопасном» списке.

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

Очевидно, что реальная версия этого будет немного длиннее (есть 119 кодов операций, 24 из которых связаны с математикой). Добавление STORE_FAST и нескольких других позволяет легко вводить данные, например 'x=5;return x+x или аналогичные, просто. Он может даже использоваться для выполнения пользовательских функций, если пользовательские функции сами выполняются через VMeval (не делайте их вызываемыми !!! или они могут где-то использоваться в качестве обратного вызова). Для обработки циклов требуется поддержка байт-кодов goto, что означает изменение с for итератора на while и сохранение указателя на текущую инструкцию, но это не слишком сложно. Что касается устойчивости к DOS, основной цикл должен проверять, сколько времени прошло с начала вычисления, а определенные операторы должны отказать в вводе данных через некоторый разумный предел (BINARY_POWER является наиболее очевидным).

Хотя этот подход несколько длиннее, чем простой синтаксический анализатор грамматики для простых выражений (см. Выше о простом захвате скомпилированной константы), он легко распространяется на более сложные входные данные и не требует работы с грамматикой (compile ничего не берет произвольно усложняется и сводит его к последовательности простых инструкций).

6 голосов
/ 04 марта 2012

Это очень запоздалый ответ, но я считаю его полезным для дальнейшего использования. Вместо того, чтобы писать свой собственный математический парсер (хотя приведенный выше пример pyparsing великолепен), вы можете использовать SymPy. У меня нет большого опыта работы с ним, но он содержит гораздо более мощный математический движок, чем кто-либо может написать для конкретного приложения, и базовая оценка выражений очень проста:

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

Очень круто! from sympy import * обеспечивает гораздо больше поддержки функций, таких как функции триггеров, специальные функции и т. Д., Но я здесь избегал этого, чтобы показать, что происходит откуда.

5 голосов
/ 28 мая 2015

Вы можете использовать модуль ast и написать NodeVisitor, который проверяет, что тип каждого узла является частью белого списка.

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)

Поскольку он работает через белый, а не черный список, он безопасен. Доступны только те функции и переменные, к которым вы явно предоставляете доступ. Я заполнил диктат математическими функциями, чтобы вы могли легко предоставить доступ к ним, если хотите, но вы должны явно использовать его.

Если строка пытается вызвать функции, которые не были предоставлены, или вызвать какие-либо методы, будет сгенерировано исключение, и оно не будет выполнено.

Поскольку он использует встроенный анализатор и анализатор Python, он также наследует правила приоритета и продвижения Python.

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

Приведенный выше код был протестирован только на Python 3.

При желании вы можете добавить декоратор тайм-аута для этой функции.

3 голосов
/ 16 декабря 2016

[Я знаю, что это старый вопрос, но стоит вспомнить новые полезные решения по мере их появления]

Начиная с python3.6, эта возможность теперь встроена в язык , придумана "f-strings" .

См .: PEP 498 - Интерполяция буквенных строк

Например (обратите внимание на префикс f):

f'{2**4}'
=> '16'
3 голосов
/ 03 марта 2010

Я думаю, что использовал бы eval(), но сначала проверил бы, чтобы убедиться, что строка является допустимым математическим выражением, а не чем-то вредоносным. Вы можете использовать регулярное выражение для проверки.

eval() также принимает дополнительные аргументы, которые можно использовать для ограничения пространства имен, в котором он работает, для большей безопасности.

0 голосов
/ 03 апреля 2019

Вот мое решение проблемы без использования eval. Работает с Python2 и Python3. Не работает с отрицательными числами.

$ 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)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...