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

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

Pieter
источник
6
^ - оператор XOR. Ожидаемое значение 6. Вероятно, вам понадобится pow (2,4).
kgiannakakis 03
25
или более питонски 2 ** 4
Фортран
1
Если вы не хотите использовать eval, единственное решение - реализовать соответствующий синтаксический анализатор грамматики. Посмотрите на pyparsing .
kgiannakakis 03

Ответы:

108

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
unutbu
источник
180

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:
JFS
источник
29
Очень классный пост, спасибо. Я взял эту концепцию и попытался создать библиотеку, которая должна быть простой в использовании: github.com/danthedeckie/simpleeval
Daniel Fairhead
можно ли это расширить для функций import math?
Hotschke
2
Учтите, что ast.parseэто небезопасно. Например ast.parse('()' * 1000000, '<string>', 'single')вылетает интерпретатор.
Антти Хаапала
1
@AnttiHaapala хороший пример. Это ошибка интерпретатора Python? В любом случае, большие объемы ввода обрабатываются тривиально, например, с помощью if len(expr) > 10000: raise ValueError.
jfs
1
@AnttiHaapala, не могли бы вы привести пример, который нельзя исправить с помощью len(expr)проверки? Или вы думаете, что в реализации Python есть ошибки, и поэтому невозможно написать безопасный код в целом?
jfs
14

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

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

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

Марк Микофски
источник
10

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

Perkins
источник
Связанный вопрос с интересным обсуждением можно найти здесь .
djvg
3
Независимо от того, .верен ли аргумент об удалении в данный момент, это оставляет возможность для уязвимостей безопасности, если в будущих версиях Python будет введен новый синтаксис, позволяющий получить доступ к небезопасным объектам или функциям каким-либо другим способом. Это решение уже небезопасно в Python 3.6 из F-строки, которые позволяют сделать следующие атаки: f"{eval('()' + chr(46) + '__class__')}". Решение, основанное на белых списках, а не на черных, будет более безопасным, но на самом деле лучше решить эту проблему evalвообще без них .
kaya3
Это отличный аргумент в отношении будущих языковых функций, представляющих новые проблемы безопасности.
Perkins
8

Вы можете использовать модуль 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.

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

Кевин
источник
7

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

Perkins
источник
6

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

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

Тим Гудман
источник
3
Но, конечно, не полагайтесь на регулярные выражения для проверки произвольных математических выражений.
High Performance Mark
@ High-Performance Mark: Да, я думаю, это зависит от того, какие математические выражения он имеет в виду. . . например, простая арифметика с числами и +, -, *, /, **, (, )или что - то более сложным
Тим Гудман
@Tim - это () меня беспокоит, а точнее ((((((())))))). По правде говоря, я думаю, что OP должен беспокоиться о них, я не нахмурился из-за проблем OP.
High Performance Mark
2
Не используйте, eval()если вы не контролируете ввод, даже если вы ограничиваете пространство имен, например, eval("9**9**9**9**9**9**9**9", {'__builtins__': None})потребляет ЦП, память.
jfs
3
Ограничение пространства имен eval не добавляет безопасности .
Антти Хаапала,
5

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

Эндибакли
источник
3
Sympy "безопасен"? Кажется, там много сообщений , в которых предполагается, что это оболочка для eval (), которую можно использовать таким же образом. Также evalfне принимает numpy ndarrays.
Марк Микофски
14
Никакой sympy не является безопасным для ненадежного ввода. Попробуйте sympy.sympify("""[].__class__.__base__.__subclasses__()[158]('ls')""")эти звонки, subprocess.Popen()которые я передалls вместо rm -rf /. Индекс, вероятно, будет другим на других компьютерах. Это вариант эксплойта Неда
Бэтчелдера
1
Действительно, безопасности это совершенно не добавляет.
Антти Хаапала,
4

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

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

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

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

f'{2**4}'
=> '16'
shx2
источник
7
Очень интересная ссылка. Но я полагаю, что f-строки здесь, чтобы упростить написание исходного кода, в то время как вопрос, похоже, касается работы со строками внутри переменных (возможно, из ненадежных источников). В этом случае нельзя использовать f-строки.
Бернхард
есть ли способ сделать что-то с эффектом f '{2 {operator} 4}', где теперь вы можете назначить оператору выполнение 2 + 4, или 2 * 4, или 2-4, и т. д.
Скайлер,
Это практически эквивалентно простому действию str(eval(...)), поэтому, конечно, не безопаснее eval.
kaya3
Похоже, то же самое с exec / eval ...
Виктор ВосМоттор благодарит Монику
0

Используйте 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
крауиоти
источник
35
eval ("(1) .__ class __.__ base __ [0] .__ subclasses __ () [81] ('echo got through'.split ())", {' builtins ': None}) # экранирует вашу песочницу
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})выполняет оболочку Борна ...
Антти Хаапала
8
Это небезопасно . Вредоносный код все еще может быть выполнен.
Fermi Paradox
This is not safe- ну, я считаю, что это так же безопасно, как и использование bash в целом. Кстати: eval('math.sqrt(2.0)')<- "математика". требуется, как написано выше.
Hannu
0

Вот мое решение проблемы без использования 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)
ГАЛЕРЕЯ ИСКУССТВ
источник