Алгоритм создания случайных, но корректных математических выражений

Чтобы сгенерировать большие тестовые данные для синтаксического анализатора выражений (на основе сортировочной станции Дейкстры), я придумал следующий скрипт Python:

#!/usr/bin/python

import ast
import sys
import random
import operator as op

def gen_digit(n):

    i = 0
    digit = ""

    if random.randint(0, 1e06) % 17 == 0:
        digit = digit + "-"

    while i < n:
        if i == 0:
            digit = digit + str(random.randint(1, 9))
        else:
            digit = digit + str(random.randint(0, 9))

        i = i + 1

    return digit;

def rnd_op():
    ops = [ "+", "-", "*", "/", "%" ]
    return ops[random.randint(0, 4)]

operators = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,
        ast.Div: op.truediv, ast.Mod: op.mod, ast.USub: op.neg}

def eval_(node):
    if isinstance(node, ast.Num):
        return node.n
    elif isinstance(node, ast.BinOp):
        return operators[type(node.op)](eval_(node.left), eval_(node.right))
    elif isinstance(node, ast.UnaryOp):
        return operators[type(node.op)](eval_(node.operand))
    else:
        raise TypeError(node)

def eval_expr(expr):
    return eval_(ast.parse(expr, mode='eval').body)

def right_op(op, expr):

    if op == "/" or op == "%":

        try:
            v = eval_expr(expr)
        except ZeroDivisionError:
            v = 0

        if v == 0:
            return op + " (" + expr + " + " + gen_digit(random.randint(1, 4)) + ")"
        else:
            return op + " " + expr

    else:
        return op + " " + expr

def gen_term():

    term = ""

    if random.randint(0, 1e06) % 17 == 0:
        term += "-"

    term += "(" + right_op(gen_digit(random.randint(1, 4)), \
            right_op(rnd_op() + " " + gen_digit(random.randint(1, 4)), \
            rnd_op() + " " + gen_digit(random.randint(1, 4)))) + ")"

    return term

def build_expr():
    return "(" + gen_term() + " " + \
        right_op(rnd_op(), gen_term()) + " " + \
        right_op(rnd_op(), gen_term()) + ")"

def rnd_expr(expr, m, d):

    if d < m:
        expr = "(" + build_expr() + " " + \
                right_op(rnd_op(), rnd_expr(expr, m, d + 1)) + " " + \
                right_op(rnd_op(), build_expr()) + ")"

    return expr

argc = len(sys.argv)

if argc > 1:

    dpth = int(sys.argv[1]);
    sys.setrecursionlimit(dpth * 10)
    print (rnd_expr(build_expr(), dpth, 0))

else:
    print (rnd_expr(build_expr(), 1, 0))

Моя реализация маневровой станции (еще один проект C++) верна и принимает четыре основных арифметических оператора плюс % (по модулю).

Я хочу, чтобы сгенерированное выражение было допустимым, но в настоящее время я иногда сталкиваюсь с делением/по модулю на ноль ошибок, несмотря на то, что пытался их избежать. Далее ast переполняется при глубине рекурсии больше 98.

Изменить: ошибки деления/по модулю на ноль возникают не в скрипте Python, а при синтаксическом анализе с помощью внешнего инструмента, такого как bc на Linux.

У кого-нибудь есть идеи, почему функция right_op или алгоритм вообще иногда дает сбой.


person Heiko Schäfer    schedule 13.02.2016    source источник
comment
Я пробовал это 1000 раз с аргументом 50, но не получил ошибку деления на ноль (python 3.5.1 на widows 7). возможно, вы могли бы предоставить random.seed для воспроизведения ошибки.   -  person miracle173    schedule 13.02.2016
comment
Деление на ноль можно обнаружить с помощью внешнего инструмента, такого как bc (калькулятор командной строки для Linux). Я не буду вдаваться в этот вопрос.   -  person Heiko Schäfer    schedule 13.02.2016
comment
Я могу использовать инструмент из основного проекта, который дает код выхода 1, если выражение неверно, поэтому я протестировал его в оболочке bash с: while(./rnd_expr.py 40 | expr2cf); do true; done   -  person Heiko Schäfer    schedule 13.02.2016


Ответы (1)


На самом деле скрипт Python делает то, для чего он предназначен: генерировать достоверные тестовые данные!

Если вы измените

def rnd_op():
    ops = [ "+", "-", "*", "/", "%" ]
    return ops[random.randint(0, 4)]

to

def rnd_op():
    ops = [ "+", "-", "*", "/", "%" ]
    return ops[random.randint(0, 3)]

т. е. опустить создание оператора по модулю %, чем следующий однострочник в оболочке bash докажет правильность:

while(./rnd_expr.py 4 | perl -e 'my $exp = <STDIN>; if(!defined(eval($exp))) { print $@." ".$exp; exit(1) } else { print eval($exp)."\n"; exit(0); }' ); do true; done

в то время как без изменения он будет жаловаться на по модулю ноль. Повторная проверка с bc показывает тот же результат.

Мой основной проект C++ в основном принимает выражения, в то время как perl и bc почти всегда их отвергают. Итак, у меня есть (вероятно) ошибка приоритета в моем основном проекте C++.

Изменить: оба верны, perl/bc и мой основной C++ проект. Первые интерпретируют результат как integer и усекают промежуточный результат, в то время как мой основной проект C++ вычисляет символические (т.е. с дробным классом).

Еще одно доказательство того, что отладка rubberduck действительно работает :)

person Heiko Schäfer    schedule 13.02.2016