【问题标题】:Is there a way to autogenerate valid arithmetic expressions?有没有办法自动生成有效的算术表达式?
【发布时间】:2017-11-15 01:11:14
【问题描述】:

我目前正在尝试创建一个 Python 脚本,该脚本将自动生成有效的以空格分隔的算术表达式。但是,我得到如下示例输出:( 32 - 42 / 95 + 24 ( ) ( 53 ) + ) 21

虽然空括号对我来说完全没问题,但我不能在计算中使用这个自动生成的表达式,因为 24 和 53 之间没有运算符,并且最后 21 之前的 + 没有第二个参数。

我想知道的是,有没有办法使用 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

【问题讨论】:

    标签: python parsing


    【解决方案1】:

    是的,您可以以 Python 方式生成随机算术表达式。不过,你需要改变你的方法。不要尝试生成字符串并计算括号。而是生成一个随机的表达式树,然后输出它。

    通过表达式树,我指的是一个名为 Expression 的类的实例,它具有子类 NumberPlusExpression,MinusExpression, 'TimesExpressionDivideExpressionParenthesizedExpression。除了Number 之外,每一个都将具有Expression 类型的字段。给每个人一个合适的__str__ 方法。生成一些随机表达式对象并打印“根”。

    你能从这里拿走还是让我编码?

    ADDENDUM:一些示例起始代码。不生成随机表达式(还没有?),但可以添加....

    # 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的算子,最大深度节点应该是概率为0的算子。
    • 我对 Python 还很陌生,所以我不确定应该如何编写 __str__ 方法。我的意思是,这是有道理的,但另一个问题是我正在使用自动生成的表达式来优化我的程序的运行时间,所以我有点需要长度计数器。
    • 是的,@Edwin,您需要提出一个有效的语法。尝试与此相反的操作:en.wikipedia.org/wiki/Parsing_expression_grammar#Examples
    • @Edwin,如果您对 Python 还很陌生,我建议您查找教程以帮助您入门。这是与类有关的一个(在 __str__ 上有一小部分):www2.lib.uchicago.edu/keith/courses/python/class/5。这里是 Python 教程的详尽列表:awaretek.com/tutorials.html.
    • @Edwin:如果您的每个运算符都有一个优先级成员,那么这样的事情应该起作用:如果任一运算符的优先级大于,则输出左右运算符周围的括号(假设它们是二进制)当前运算符的优先级。然后只需在它们之间添加当前操作符符号。无法在评论中给出代码示例。
    【解决方案2】:

    实际上,只要 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 可以是单个输入,也可以是来自较低子树的输入。
    【解决方案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 是可能的,尽管通过异常处理这些可以被过滤掉。

    【讨论】:

      【解决方案4】:

      我在一个类似的任务中发现了这个线程,即为符号计算的单元测试生成随机表达式。在我的版本中,我包含了一元函数并允许符号是任意字符串,即您可以使用数字或变量名。

      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)
      

      【讨论】:

        【解决方案5】:
        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)
        

        【讨论】:

          【解决方案6】:

          在 RPN 中随机生成一个数组,其中包含运算符和数字的混合(始终有效)。然后从数组中间开始,生成对应的求值树。

          【讨论】:

            猜你喜欢
            • 2012-03-27
            • 2020-11-13
            • 2013-04-24
            • 2011-03-16
            • 1970-01-01
            • 2015-05-05
            • 1970-01-01
            • 2010-10-01
            • 1970-01-01
            相关资源
            最近更新 更多