有没有一种方法可以自动生成有效的算术表达式?

10
我正在尝试创建一个Python脚本,自动生成有效的空格分隔算术表达式。然而,我的样例输出如下: ( 32 - 42 / 95 + 24 ( ) ( 53 ) + ) 21 虽然空括号对我来说完全没问题,但我不能在计算中使用这个自动生成的表达式,因为24和53之间没有运算符,并且末尾的+号没有第二个参数。
我想知道的是,是否有一种Pythonic的解决方案来解决/修复这些错误?(在任何人指出之前,我首先要承认我所发布的代码可能是我推送过的最糟糕的代码,也基本上不符合Python的核心原则。)
import random
parentheses = ['(',')']
ops = ['+','-','*','/'] + parentheses

lines = 0

while lines < 1000:
    fname = open('test.txt','a')
    expr = []
    numExpr = lines
    if (numExpr % 2 == 0):
        numExpr += 1
    isDiv = False # Boolean var, makes sure there's no Div by 0

    # isNumber, isParentheses, isOp determine whether next element is a number, parentheses, or operator, respectively
    isNumber = random.randint(0,1) == 0 # determines whether to start sequence with number or parentheses
    isParentheses = not isNumber
    isOp = False
    # Counts parentheses to ensure parentheses are matching
    numParentheses = 0
    while (numExpr > 0 or numParentheses > 0):
        if (numExpr < 0 and numParentheses > 0):
            isDiv = False
            expr.append(')')
            numParentheses -= 1
        elif (isOp and numParentheses > 0):
            rand = random.randint(0,5)
            expr.append(ops[rand])
            isDiv = (rand == 3) # True if div op was just appended
            # Checks to see if ')' was appended
            if (rand == 5):
                isNumber = False
                isOp = True
                numParentheses -= 1
            # Checks to see if '(' was appended
            elif (rand == 4):
                isNumber = True
                isOp = False
                numParentheses += 1
            # All other operations go here
            else:
                isNumber = True
                isOp = False
        # Didn't add parentheses possibility here in case expression in parentheses somehow reaches 0
        elif (isNumber and isDiv):
            expr.append(str(random.randint(1,100)))
            isDiv = False
            isNumber = False
            isOp = True
        # If a number's up, decides whether to append parentheses or a number
        elif (isNumber):
            rand = random.randint(0,1)
            if (rand == 0):
                expr.append(str(random.randint(0,100)))
                isNumber = False
                isOp = True
            elif (rand == 1):
                if (numParentheses == 0):
                    expr.append('(')
                    numParentheses += 1
                else:
                    rand = random.randint(0,1)
                    expr.append(parentheses[rand])
                    if rand == 0:
                        numParentheses += 1
                    else:
                        numParentheses -= 1
            isDiv = False
        numExpr -= 1

    fname.write(' '.join(expr) + '\n')
    fname.close()
    lines += 1
6个回答

17
是的,你可以用 Pythonic 的方法生成随机算术表达式。 但你需要改变你的方法。 不要试图生成一个字符串并计算括号。 相反,生成一个随机的表达式树,然后输出它。
通过表达式树,我指的是一个名为 Expression 的类的实例,其子类为 Number、PlusExpression、MinusExpression、TimesExpression、DivideExpression 和 ParenthesizedExpression。 这些中的每一个(除了 Number)都将有类型为 Expression 的字段。 给每个添加一个合适的 __str__ 方法。 生成一些随机的表达式对象,然后只打印 "root"。
你能完成下面的任务吗?或者你想让我编码?
补充说明:一些样例起始代码。 尚未生成随机表达式,但可以添加....
# This is just the very beginning of a script that can be used to process
# arithmetic expressions.  At the moment it just defines a few classes
# and prints a couple example expressions.

# Possible additions include methods to evaluate expressions and generate
# some random expressions.

class Expression:
    pass

class Number(Expression):
    def __init__(self, num):
        self.num = num

    def __str__(self):
        return str(self.num)

class BinaryExpression(Expression):
    def __init__(self, left, op, right):
        self.left = left
        self.op = op
        self.right = right

    def __str__(self):
        return str(self.left) + " " + self.op + " "  + str(self.right)

class ParenthesizedExpression(Expression):
    def __init__(self, exp):
        self.exp = exp

    def __str__(self):
        return "(" + str(self.exp) + ")"

e1 = Number(5)
print e1

e2 = BinaryExpression(Number(8), "+", ParenthesizedExpression(BinaryExpression(Number(7), "*", e1)))
print e2

** 附录2 **

重新学习Python真的很有趣。我忍不住实现了随机表达式生成器。它基于上面的代码构建。对于硬编码造成的困扰,我深感抱歉!

from random import random, randint, choice

def randomExpression(prob):
    p = random()
    if p > prob:
        return Number(randint(1, 100))
    elif randint(0, 1) == 0:
        return ParenthesizedExpression(randomExpression(prob / 1.2))
    else:
        left = randomExpression(prob / 1.2)
        op = choice(["+", "-", "*", "/"])
        right = randomExpression(prob / 1.2)
        return BinaryExpression(left, op, right)

for i in range(10):
    print(randomExpression(1))

这是我得到的输出:

(23)
86 + 84 + 87 / (96 - 46) / 59
((((49)))) + ((46))
76 + 18 + 4 - (98) - 7 / 15
(((73)))
(55) - (54) * 55 + 92 - 13 - ((36))
(78) - (7 / 56 * 33)
(81) - 18 * (((8)) * 59 - 14)
(((89)))
(59)

看起来不太好看。我认为它生成了太多的括号。也许改变在选择括号表达式和二进制表达式之间的概率可能会有所帮助...


1
除了要补充的是,操作符被添加到树中的概率应该随着路径深度的增加而减小。根节点应该是一个概率为1的操作符,而最大深度节点应该是一个概率为0的操作符。 - Codie CodeMonkey
我对Python还比较陌生,不太确定应该如何编写__str__方法。我的意思是,这很有道理,但另一个问题是我正在使用自动生成的表达式来优化程序的运行时间,所以我需要长度计数器。 - Edwin
@DeepYellow +1 对于概率观察的认同。 - Ray Toal
1
@Edwin,如果你对Python很新,我建议你查找教程来入门。这里有一个关于类(包括一小部分__str__)的教程:http://www2.lib.uchicago.edu/keith/courses/python/class/5。还有这是一个详尽的Python教程列表:http://www.awaretek.com/tutorials.html。 - TJ Koblentz
1
@Edwin:如果你的每个运算符都有一个优先级成员,那么像这样做应该可以解决问题:在左右运算符周围输出括号(假设它们是二元的),如果任何一个运算符的优先级大于当前运算符的优先级。然后只需在它们之间添加当前运算符标志即可。无法在评论中提供代码示例。 - Codie CodeMonkey
显示剩余7条评论

5

我在一个类似的问题上找到了这个线程,即为符号计算单元测试生成随机表达式。在我的版本中,我包括一元函数并允许使用任意字符串作为符号,例如你可以使用数字或变量名。

from random import random, choice

UNARIES = ["sqrt(%s)", "exp(%s)", "log(%s)", "sin(%s)", "cos(%s)", "tan(%s)",
           "sinh(%s)", "cosh(%s)", "tanh(%s)", "asin(%s)", "acos(%s)",
           "atan(%s)", "-%s"]
BINARIES = ["%s + %s", "%s - %s", "%s * %s", "%s / %s", "%s ** %s"]

PROP_PARANTHESIS = 0.3
PROP_BINARY = 0.7

def generate_expressions(scope, num_exp, num_ops):
    scope = list(scope) # make a copy first, append as we go
    for _ in xrange(num_ops):
        if random() < PROP_BINARY: # decide unary or binary operator
            ex = choice(BINARIES) % (choice(scope), choice(scope))
            if random() < PROP_PARANTHESIS:
                ex = "(%s)" % ex
            scope.append(ex)
        else:
            scope.append(choice(UNARIES) % choice(scope))
    return scope[-num_exp:] # return most recent expressions

如前面的答案所述,我只是在二元运算符周围加了一些括号,其概率为PROP_PARANTHESIS(这有点作弊)。二元运算符比一元运算符更常见,因此我也留下了它以供配置(PROP_BINARY)。示例代码如下:

scope = [c for c in "abcde"]
for expression in generate_expressions(scope, 10, 50):
    print expression

这将生成类似下面的内容:
e / acos(tan(a)) / a * acos(tan(a)) ** (acos(tan(a)) / a + a) + (d ** b + a)
(a + (a ** sqrt(e)))
acos((b / acos(tan(a)) / a + d) / (a ** sqrt(e)) * (a ** sinh(b) / b))
sin(atan(acos(tan(a)) ** (acos(tan(a)) / a + a) + (d ** b + a)))
sin((b / acos(tan(a)) / a + d)) / (a ** sinh(b) / b)
exp(acos(tan(a)) / a + acos(e))
tan((b / acos(tan(a)) / a + d))
acos(tan(a)) / a * acos(tan(a)) ** (acos(tan(a)) / a + a) + (d ** b + a) + cos(sqrt(e))
(acos(tan(a)) / a + acos(e) * a + e)
((b / acos(tan(a)) / a + d) - cos(sqrt(e))) + sinh(b)

PROP_BINARY = 1.0放置并应用

scope = range(100)

这让我们回到像这样的输出。
43 * (50 * 83)
34 / (29 / 24)
66 / 47 - 52
((88 ** 38) ** 40)
34 / (29 / 24) - 27
(16 + 36 ** 29)
55 ** 95
70 + 28
6 * 32
(52 * 2 ** 37)

4

实际上,只要Ray Toal的回答形式上是正确的,对于这样一个简单的问题,您不必为每个操作符*子类化。我想出了以下代码,它工作得非常好:

import random
import math


class Expression(object):
    OPS = ['+', '-', '*', '/']

    GROUP_PROB = 0.3

    MIN_NUM, MAX_NUM = 0, 20

    def __init__(self, maxNumbers, _maxdepth=None, _depth=0):
        """
        maxNumbers has to be a power of 2
        """
        if _maxdepth is None:
            _maxdepth = math.log(maxNumbers, 2) - 1

        if _depth < _maxdepth and random.randint(0, _maxdepth) > _depth:
            self.left = Expression(maxNumbers, _maxdepth, _depth + 1)
        else:
            self.left = random.randint(Expression.MIN_NUM, Expression.MAX_NUM)

        if _depth < _maxdepth and random.randint(0, _maxdepth) > _depth:
            self.right = Expression(maxNumbers, _maxdepth, _depth + 1)
        else:
            self.right = random.randint(Expression.MIN_NUM, Expression.MAX_NUM)

        self.grouped = random.random() < Expression.GROUP_PROB
        self.operator = random.choice(Expression.OPS)

    def __str__(self):
        s = '{0!s} {1} {2!s}'.format(self.left, self.operator, self.right)
        if self.grouped:
            return '({0})'.format(s)
        else:
            return s


for i in range(10):
    print Expression(4)

虽然目前没有处理零除的情况,但可以改进以考虑到这样的情况,通过属性自定义所有参数,并允许为maxNumbers参数设置任何值等。

* “简单问题”是指“生成有效表达式”; 如果您添加了任何其他功能(例如表达式评估),那么Ray的方法将会很有用,因为您可以以更清晰的方式定义每个子类的行为。

编辑(输出):

(5 * 12 / 16)
6 * 3 + 14 + 0
13 + 15 - 1
19 + (8 / 8)
(12 + 3 - 5)
(4 * 0 / 4)
1 - 18 / (3 * 15)
(3 * 16 + 3 * 1)
(6 + 16) / 16
(8 * 10)

我真的很喜欢这个答案,是否有可能添加一元运算符,例如只接受一个输入的运算符?就像Sqrt(x)一样,其中x可以是单个输入或来自较低子树的输入。 - user1234440

3

好的,我无法抵制使用我们在Ray的答案中讨论的一些想法来添加自己的实现。 我对一些事情的处理方式与Ray不同。

我增加了一些处理每个运算符发生概率的代码。 运算符是有偏差的,因此优先级较低的运算符(较大的优先级值)比较高阶的运算符更常见。

当优先级要求时,我仅实现了括号。 由于整数具有最高优先级(最低的优先级值),它们永远不会被包含在括号中。 表达式树中不需要一个带括号的表达式节点。

使用运算符的概率偏向于初始级别(使用二次函数)以获得更好的运算符分布。 选择不同的指数可以更好地控制输出质量,但我没有过多尝试这些可能性。

我进一步实现了一个评估器,以便娱乐并过滤出不确定的表达式。

import sys
import random

# dictionary of operator precedence and incidence probability, with an
# evaluator added just for fun.
operators = {
    '^': {'prec': 10, 'prob': .1, 'eval': lambda a, b: pow(a, b)},
    '*': {'prec': 20, 'prob': .2, 'eval': lambda a, b: a*b},
    '/': {'prec': 20, 'prob': .2, 'eval': lambda a, b: a/b},
    '+': {'prec': 30, 'prob': .25, 'eval': lambda a, b: a+b},
    '-': {'prec': 30, 'prob': .25, 'eval': lambda a, b: a-b}}

max_levels = 3
integer_range = (-100, 100)
random.seed()

# A node in an expression tree
class expression(object):
    def __init__(self):
        super(expression, self).__init__()

    def precedence(self):
        return -1

    def eval(self):
        return 0

    @classmethod
    def create_random(cls, level):
        if level == 0:
            is_op = True
        elif level == max_levels:
            is_op = False
        else:
            is_op = random.random() <= 1.0 - pow(level/max_levels, 2.0)

        if is_op:
            return binary_expression.create_random(level)
        else:
            return integer_expression.create_random(level)

class integer_expression(expression):
    def __init__(self, value):
        super(integer_expression, self).__init__()

        self.value = value

    def __str__(self):
        return self.value.__str__()

    def precedence(self):
        return 0

    def eval(self):
        return self.value

    @classmethod
    def create_random(cls, level):
        return integer_expression(random.randint(integer_range[0],
                                                 integer_range[1]))

class binary_expression(expression):
    def __init__(self, symbol, left_expression, right_expression):
        super(binary_expression, self).__init__()

        self.symbol = symbol
        self.left = left_expression
        self.right = right_expression

    def eval(self):
        f = operators[self.symbol]['eval']
        return f(self.left.eval(), self.right.eval())

    @classmethod
    def create_random(cls, level):
        symbol = None

        # Choose an operator based on its probability distribution
        r = random.random()
        cumulative = 0.0
        for k, v in operators.items():
            cumulative += v['prob']
            if r <= cumulative:
                symbol = k
                break

        assert symbol != None

        left = expression.create_random(level + 1)
        right = expression.create_random(level + 1)

        return binary_expression(symbol, left, right)

    def precedence(self):
        return operators[self.symbol]['prec']

    def __str__(self):
        left_str = self.left.__str__()
        right_str = self.right.__str__()
        op_str = self.symbol

        # Use precedence to determine if we need to put the sub expressions in
        # parentheses
        if self.left.precedence() > self.precedence():
            left_str = '('+left_str+')'
        if self.right.precedence() > self.precedence():
            right_str = '('+right_str+')'

        # Nice to have space around low precedence operators
        if operators[self.symbol]['prec'] >= 30:
            op_str = ' ' + op_str + ' '

        return left_str + op_str + right_str

max_result = pow(10, 10)
for i in range(10):
    expr = expression.create_random(0)

    try:
        value = float(expr.eval())
    except:
        value = 'indeterminate'

    print expr, '=', value

我得到了这些结果:
(4 + 100)*41/46 - 31 - 18 - 2^-83 = -13.0
(43 - -77)/37^-94 + (-66*67)^(-24*49) = 3.09131533541e+149
-32 - -1 + 74 + 74 - 15 + 64 - -22/98 = 37.0
(-91*-4*45*-55)^(-9^2/(82 - -53)) = 1.0
-72*-85*(75 - 65) + -100*19/48*22 = 61198.0
-57 - -76 - -54*76 + -38 - -23 + -17 - 3 = 4088.0
(84*-19)^(13 - 87) - -10*-84*(-28 + -49) = 64680.0
-69 - -8 - -81^-51 + (53 + 80)^(99 - 48) = 2.07220963807e+108
(-42*-45)^(12/87) - -98 + -23 + -67 - -37 = 152.0
-31/-2*-58^-60 - 33 - -49 - 46/12 = -79.0

程序有一些行为,虽然是有效的,但人类不会这样做。例如:
  1. 它可以创建长字符串的连续分数(例如1/2/3/4/5)。
  2. 负数的加减法很常见(例如1- -2)
这些可以通过清理步骤进行纠正。
此外,无法保证答案是确定的。除以0和0^0都是有可能的,但是通过异常处理这些情况可以被过滤掉。

3
import random

def expr(depth):
    if depth==1 or random.random()<1.0/(2**depth-1): 
        return str(int(random.random() * 100))
    return '(' + expr(depth-1) + random.choice(['+','-','*','/']) + expr(depth-1) + ')'

for i in range(10):
    print expr(4)

1
使用运算符和数字的混合随机生成一个RPN数组(始终有效)。然后从数组中间开始生成相应的评估树。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接