Оценка математического выражения в строке

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

Ответ 1

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

Ответ 2

eval is evil

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

Примечание: даже если вы используете set __builtins__ to 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:

Ответ 3

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

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

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

Ответ 4

Хорошо, поэтому проблема с 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 в список вещей, которым разрешено следовать ., но зачем беспокоиться?

Ответ 5

Причина 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

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

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

Ответ 7

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

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)

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

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

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

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

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

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

Ответ 8

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

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

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

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

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

Ответ 9

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

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

Ответ 10

Если вы не хотите использовать eval, то единственным решением является реализация соответствующего анализатора грамматики. Посмотрите pyparsing.

Ответ 11

Используйте eval в чистом пространстве имен:

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

Чистое пространство имен должно предотвращать инъекцию. Например:

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

В противном случае вы получите:

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

Возможно, вы захотите предоставить доступ к математическому модулю:

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

Ответ 13

Если вы уже используете wolframalpha, у них есть python api, который позволяет вам оценивать выражения. Может быть немного медленнее, но, по крайней мере, очень точно.

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