Оценка математического выражения в строке
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
можно обойти это, но нет ли лучшего и, что еще важнее, более безопасного метода для оценки математического выражения, хранящегося в строке?
18 ответов
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
eval
это зло
eval("__import__('os').remove('important file')") # arbitrary commands
eval("9**9**9**9**9**9**9**9", {'__builtins__': None}) # CPU, memory
Примечание: даже если вы используете set __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:
Некоторые более безопасные альтернативы eval()
а также sympy.sympify().evalf()
*:
* SymPy sympify
также небезопасно в соответствии со следующим предупреждением из документации.
Предупреждение: обратите внимание, что эта функция использует
eval
и, следовательно, не должны использоваться на неанизированном входе.
Причина 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
взять что-нибудь произвольно сложное и сводит его к последовательности простых инструкций).
Итак, проблема с 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
к списку вещей, которым можно следовать .
а зачем?
Вы можете использовать модуль 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.
При желании вы можете добавить декоратор времени ожидания для этой функции.
Основываясь на удивительном подходе Перкинса , я обновил и улучшил его «ярлык» для простых алгебраических выражений (без функций или переменных). Теперь он работает на Python 3.6+ и избегает некоторых подводных камней:
import re, sys
# Kept outside simple_eval() just for performance
_re_simple_eval = re.compile(rb'd([\x00-\xFF]+)S\x00')
def simple_eval(expr):
c = compile(expr, 'userinput', 'eval')
m = _re_simple_eval.fullmatch(c.co_code)
if not m:
raise ValueError(f"Not a simple algebraic expresion: {expr}")
return c.co_consts[int.from_bytes(m.group(1), sys.byteorder)]
Тестирование с использованием некоторых примеров из других ответов:
for expr, res in (
('2^4', 6 ),
('2**4', 16 ),
('1 + 2*3**(4^5) / (6 + -7)', -5.0 ),
('7 + 9 * (2 << 2)', 79 ),
('6 // 2 + 0.0', 3.0 ),
('2+3', 5 ),
('6+4/2*2', 10.0 ),
('3+2.45/8', 3.30625),
('3**3*3/3+3', 30.0 ),
):
result = simple_eval(expr)
ok = (result == res and type(result) == type(res))
print("{} {} = {}".format("OK!" if ok else "FAIL!", expr, result))
OK! 2^4 = 6
OK! 2**4 = 16
OK! 1 + 2*3**(4^5) / (6 + -7) = -5.0
OK! 7 + 9 * (2 << 2) = 79
OK! 6 // 2 + 0.0 = 3.0
OK! 2+3 = 5
OK! 6+4/2*2 = 10.0
OK! 3+2.45/8 = 3.30625
OK! 3**3*3/3+3 = 30.0
[Я знаю, что это старый вопрос, но стоит указать новые полезные решения, когда они появятся]
Начиная с python3.6, эта возможность теперь встроена в язык, придуманный "f-strings".
См.: PEP 498 - Интерполяция буквенных строк
Например (обратите внимание на f
префикс):
f'{2**4}'
=> '16'
Я думаю, что я бы использовал eval()
, но сначала проверим, чтобы убедиться, что строка является допустимым математическим выражением, а не чем-то вредоносным. Вы можете использовать регулярное выражение для проверки.
eval()
также принимает дополнительные аргументы, которые можно использовать для ограничения пространства имен, в котором он работает, для большей безопасности.
Это очень запоздалый ответ, но я считаю его полезным для дальнейшего использования. Вместо того, чтобы писать свой собственный математический парсер (хотя приведенный выше пример 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 *
вносит гораздо больше поддержки функций, таких как функции триггеров, специальные функции и т. д., но я избегал этого здесь, чтобы показать, что происходит откуда.
Использование 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
Использование библиотеки парсера lark https://stackoverflow.com/posts/67491514/edit
from operator import add, sub, mul, truediv, neg, pow
from lark import Lark, Transformer, v_args
calc_grammar = f"""
?start: sum
?sum: product
| sum "+" product -> {add.__name__}
| sum "-" product -> {sub.__name__}
?product: power
| product "*" power -> {mul.__name__}
| product "/" power -> {truediv.__name__}
?power: atom
| power "^" atom -> {pow.__name__}
?atom: NUMBER -> number
| "-" atom -> {neg.__name__}
| "(" sum ")"
%import common.NUMBER
%import common.WS_INLINE
%ignore WS_INLINE
"""
@v_args(inline=True)
class CalculateTree(Transformer):
add = add
sub = sub
neg = neg
mul = mul
truediv = truediv
pow = pow
number = float
calc_parser = Lark(calc_grammar, parser="lalr", transformer=CalculateTree())
calc = calc_parser.parse
def eval_expr(expression: str) -> float:
return calc(expression)
print(eval_expr("2^4"))
print(eval_expr("-1*2^4"))
print(eval_expr("-2^3 + 1"))
print(eval_expr("2**4")) # Error
Если вы не хотите использовать eval, единственное решение - реализовать соответствующий синтаксический анализатор грамматики. Посмотрите на pyparsing.
Я пришел сюда в поисках парсера математических выражений. Читая некоторые ответы и просматривая библиотеки, я наткнулся на py-выражение , которое сейчас использую. В основном он обрабатывает множество операторов и конструкций формул, но если вам что-то не хватает, вы можете легко добавить к нему новые операторы/функции.
Основной синтаксис:
from py_expression.core import Exp
exp = Exp()
parsed_formula = exp.parse('a+4')
result = exp.eval(parsed_formula, {"a":2})
Единственная проблема, с которой я столкнулся до сих пор, заключается в том, что в нем нет ни встроенных математических констант, ни механизма их добавления. Однако я просто предложил решение этой проблемы: https://github.com/FlavioLionelRita/py-expression/issues/7
Если вы уже используете wolframalpha, у них есть API Python, который позволяет вам оценивать выражения. Может быть немного медленно, но, по крайней мере, очень точно.
Вот мое решение проблемы без использования 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)
А как насчет реализации собственного решателя выражений, адаптированного к вашей конкретной проблеме? С помощью средства решения выражений scinumtools вы можете использовать уже существующие операторы или создать свои собственные с нуля.
from scinumtools.solver import *
with ExpressionSolver(AtomBase) as es:
es.solve("sin(23) < 1 && 3*2 == 6 || !(23 > 43) && cos(0) == 1")
Вот небольшой пример создания собственных операторов:
class OperatorSquare(OperatorBase): # operate from left side
symbol: str = '~'
def operate_unary(self, tokens):
right = tokens.get_right()
tokens.put_left(right*right)
class OperatorCube(OperatorBase): # operate from right side
symbol: str = '^'
def operate_unary(self, tokens):
left = tokens.get_left()
tokens.put_left(left*left*left)
operators ={'square':OperatorSquare,'cube':OperatorCube,'add':OperatorAdd}
steps = [
dict(operators=['square','cube'], otype=Otype.UNARY),
dict(operators=['add'], otype=Otype.BINARY),
]
with ExpressionSolver(AtomBase, operators, steps) as es:
es.solve('~3 + 2^')
# will result in: Atom(17)
Вы даже можете изменить поведение атома:
class AtomCustom(AtomBase):
value: str
def __init__(self, value:str):
self.value = str(value)
def __add__(self, other):
return AtomCustom(self.value + other.value)
def __gt__(self, other):
return AtomCustom(len(self.value) > len(other.value))
operators = {'add':OperatorAdd,'gt':OperatorGt,'par':OperatorPar}
steps = [
dict(operators=['par'], otype=Otype.ARGS),
dict(operators=['add'], otype=Otype.BINARY),
dict(operators=['gt'], otype=Otype.BINARY),
]
with ExpressionSolver(AtomCustom, operators, steps) as es:
es.solve("(limit + 100 km/s) > (limit + 50000000000 km/s)")
# will result in: Atom('False')
Кстати, я являюсь автором и мне будет очень интересно услышать ваши комментарии и предложения в адрес проекта scinumtools. Проверьте это на GitHub или PyPi.
В Python уже есть функция для безопасной оценки строк, содержащих буквенные выражения: