Генерация рандомных ветвлений на Питоне
Вспоминая Докинза, основную идею можно выразить так: если долго держать смерч над помойкой, то может собраться Боинг-747. Появление структуры из хаоса дуриком: перебирая и рекомбинируя всё подряд, из всех бессмысленных и беспорядочных процессов можно увидеть вполне осмысленные и упорядоченные. Если такие процессы каким-либо образом закрепляются и повторяются, то система, еще вчера представлявшая из себя броуновское движение, сегодня начинает выглядеть уже так, как будто ее поведение настроила невидимая рука, и что она совершает какие-то осмысленные с нашей точки зрения действия. При этом никакой руки и близко нет. Она настроила себя сама.
Чтобы в этом убедиться еще раз, я и стремлюсь написать некое подобие цифровой жизни, которая из хаоса и без лишних указаний от человека способна будет сама себе рандомно генерить логику и существовать по ней в своем естественном пространстве обитания — операционной системе. Да, в этом, вероятно, есть отличие от многих программ из направления «Искусственная жизнь», которые «живут» в загончиках, плодят «хищников» и «травоядных», и со-существуют на искусственных полях с «едой» и друг другом. Никакие из этих программ не взаимодействуют с объектами системы (процессами, файлами и т.д.), а значит код по-настоящему не живет. Кроме того, этот код так или иначе всё равно выполняет какую-то нужную человеку задачу и очень из-за этого ограничен рамками.
Чтобы реализовать код с большой степенью свободы действий в операционной системе, который при этом не представлял бы из себя просто хаотический набор исполняющихся инструкций, появилась модель, которая состоит из 3 модулей.
- Модуль рандомной генерации основного исполняемого кода
- Модуль, связанный с рандомным образованием опыта
- Модуль «компьютерного зрения» объектов ОС
В этой статье пойдет речь о первом модуле, который пока что представляет из себя только генерацию рандомных ветвлений, т.е. конструкций типа «if-elif-else». Почему ветвления? Потому что по-большому счёту жизнь любого живого организма состоит из условных реакций: всё, что мы делаем, является ответом на воспринимаемую информацию. Клетки делятся, если наступают определенные условия, жертва пытается бежать, если видит более сильного хищника, а если он слабее, то может попытаться его атаковать, тараканы разбегаются, если включается свет, человек идет есть, если голоден и т.д. и т.п. — этот ряд бесконечен. Нет каких-либо независимых, отдельных действий, которые ничем не обусловлены. Следовательно, поведение, в частности, живых организмов описывается как реакция на условие: IF [что-то] THEN [что-то]. Это поведение мы и пытаемся генерировать.
Почему рандомно? Для того, чтобы оставить коду максимальную возможность действовать самостоятельно и отодвинуть от этого процесса человека(программиста) как можно дальше (в идеале исключить совсем). Последнее — наиболее сложное для программиста, т.к. стандартное программирование, к которому все привыкли, напоминает жесткую дрессировку животных, которые должны выполнять ровно то, что укажет программист, ровно так, как он укажет и тогда, когда он укажет. Тут ситуация обратная: конечный генерируемый код должен действовать так, чтобы это было для создателя его генератора максимально непредсказуемым.
Прежде, чем мы перейдем к схемам и коду генератора, необходимо остановиться на функции принятия решений, которая используется как дирижер, давая исполняться то одной, то другой части кода. Ранее я уже писал о ней здесь. Мне тогда подсказали, что я описал идею Reinforcement Learning и игру Джона Конвеея под названием «Жизнь». Вполне может быть, я не имею ничего против того, чтобы использовать то, что уже наработано или открыто. В конце концов всё новое — это синтез уже известного, да я и сам признавал, что перенял идею приоритезации потоков, которая используется в винде. Здесь она очень подходит.
В настоящее время упомянутая функция немного трансформировалась:
def make_solution(p_random, p_deter):
deter_flag = 0
random_flag = 0
if p_random >= random.random():
p_random-=0.01 # баланс приоритета
p_deter+=0.01
random_flag = 1
if p_deter >= random.random():
p_deter-=0.01 # баланс приоритета
p_random+=0.01
deter_flag = 1
if random_flag == 1 and deter_flag == 0:
return(p_random, p_deter, 1)
elif deter_flag == 1 and random_flag == 0:
return(p_random, p_deter, -1)
else:
return (p_random, p_deter,0)
На вход она принимает 2 вероятности (по умолчанию на старте они обе равны 0,5), после чего поочередно проверяет их срабатывание. Сработавшая вероятность уменьшает сама себя на 1% и одновременно увеличивает на 1% другую. Следовательно, с каждым разом, как вероятность срабатывает, она уменьшается, а другая — увеличивается. В результате ни одна вероятность не получает слишком большого преимущества перед другой, и они самобалансируются, образуя нормальное распределение с центром в 0,5 и с рабочей окрестностью не более +-10%, что отличает эту функцию от стандартной random, где вероятность в нашем случае всегда была бы равна 0,5 и не зависела бы от предыдущих вычислений.
Образно говоря, это маятник вероятности с небольшой амплитудой. Если сработала первая вероятность и не сработала вторая — возвращается 1, в обратном случае возвращается -1, а если сработали или не сработали обе — 0. Таким образом, функция make_solution на 2 входящих вероятности возвращает одно из 3 возможных действий, давая, соответственно принимать сбалансированное решение на развилках с 3 возможными вариантами продолжения. В дальнейшем эта функция, скорее всего будет универсальной, и сможет принимать неопределенное количество вероятностей, т.к. вариативность на развилках может быть больше 3, но в случае с генератором if-elif-else трёх вариантов продолжения вполне достаточно.
Здесь надо еще отметить, что в коде встречаются разные, так сказать, типовые развилки. Например, как будет видно ниже, в основной функции генератора находится развилка, в которой идет выбор схемы построения ветвления, коих всего 3, но в коде также присутствуют другие случаи: вставлять блок действия или запускать рекурсию, сколько строк действия генерировать, насколько сложной должна быть строка с условием, ставить or или and, elif или else.
Я считаю, что вероятностный маятник, о котором мы говорили выше, должен выставляться для каждого типа действия свой: тогда развилка балансируется только исходя из того, что происходило ранее именно на этой развилке, а не в каких то других частях кода. Т.е. при выборе общей структуры ветвления у нас своя пара вероятностей, а внутри, когда строятся его элементы — другая.
Можно конечно балансировать все действия одной парой, но тогда вероятность на каждой развилке будет очень сложной и будет зависеть от всех предыдущих действий на других развязках. Рандомность такой конструкции будет еще выше, но я лично пока что склоняюсь к первой схеме, ибо мне нравится конструкция, где в рамках одного большого качающегося маятника качаются другие маленькие, т.е. в одном большом балансе рождаются «балансы» поменьше. Плюс к этому во второй схеме рандомность тоже более чем достаточная.
При написании генератора ветвлений нужно было сделать не только работоспособный код, который выдает безошибочные генерации, но и такой код, который может генерировать максимально возможные конструкции if-elif-else, а таких возможных вариантов не 2 и не 3. Рассмотрим, например, следующие возможные схемы.
Под значком [..] в схемах я подразумеваю набор выражений для условия либо блок рандомных действий. Самая элементарная схема — 1, где просто идет условие, а за ним блок действия. 2а и 2b — это вариации if с одним elif или одним else. В варианте 2с if идет уже в комбинации с несколькими elif без еlse. И, наконец, в варианте 2d представлена наиболее общая схема, где if содержит в себе несколько elif и 1 else.
Всё было бы просто, если бы не необходимость построения неограниченных ветвлений. После каждого if, elif или else может вызываться рекурсия, которая в свою очередь также может рекурсировать дальше и плодить «вправо» новые блоки elif-else. Посмотрим на схему возможных вариантов.
В вариантах 2е и 2f показаны простые частные случаи такого рекурсивного ветвления, когда рекурсия вызывается либо после одиночного elif, либо после одиночного else. Вариант 2g описывает наиболее сложный и общий случай такой рекурсии, когда после каждого elif может идти блок действия+рекурсия (либо сразу рекурсия), и тоже самое может происходить после else.
Есть еще вариации, когда рекурсия наступает сразу же после if или после if и блока действия.
Это видно в вариантах 3а и 3b. Вариант 3с показывает такую схему в наиболее общем виде.
Нельзя сказать, что приведенные выше схемы охватывают все возможные варианты построения ветвлений, однако даже в таком виде конечный код легко рождает ветвления в 150 строк, уходя «вправо» на 10-15 шагов. В любом случае усложнить схемы при необходимости не составляет большого труда.
Можно посмотреть на пример одной такой генерации, чтобы убедиться в том, что ветвления могут быть весьма разнообразными.
На состав условных выражений и блоков действий обращать внимания не нужно — для визуальной простоты они сгенерированы всего из комбинаций двух переменных, 3 выражений и небольшого ряда арифметических и логических знаков. Обсуждение реального «мяса» для рекомбинаций выходит за рамки данной статьи (об этом речь будет при рассмотрении 3 модуля).
Перед тем, как перейти к непосредственному рассмотрению кода генератора, необходимо вспомнить, что сгенерированные блоки должны смещаться вправо по горизонтали, если это elif, else, рекурсия if или блоки действия, а также «возвращаться» назад влево, после того как ветка завершится. Причем, учитывая, что Питон весьма придирчив к горизонтальным отступам, желательно сделать шаг одинаковым (в нашем случае шаг равен 3).
На следующей схеме проиллюстрировано, каким образом сдвигаются смещения.
Самое главное тут, что смещения при углублении ветвления сдвигаются всегда вправо. Однако, если у нас есть, например, блок elif-else, в котором есть несколько elif или одинарная пара elif-else, то существует необходимость «возврата» каретки, уплывшей вправо, для того, чтобы следующий elif (или else) начинался с того же смещения, что и предыдущий в блоке. Для этого необходимо сохранять изначальное смещение (wall_offset) и после окончания генерации ветки (например, полного ветвления одного elif), восстанавливать его. Таким образом достигается ровное нахождение «друг над другом» элементов elif, else в блоке. Причем у каждого нового блока это смещение своё. Тот же самый приём обеспечивает стройность общей конструкции if-elif-else (в том числе при рекурсиях).
Теперь перейдем к коду. Код общим объемом около 200 строк состоит из 8 функций, одну из которых мы рассмотрели выше. Из-за рекурсивности и большого количества параметров, передаваемых в функции, он местами может быть плохо читаемым. Для начала приведу то самое «мясо», которое используется для генерации условных выражений и блоков действий.
var_list = ['a','b']
exp_list = ['a+b','b-a', 'b//a']
sign = ['+','-','/','*','//']
sign2 = ['>','<','==','>=','<=','!=']
a = 3
b = 2
Как видно, используются две переменные: a и b (var_list), которые инициализированы, 3 арифметических выражения (exp_list), а также два листа со знаками (sign, sign2). Как и говорилось ранее, состав получаемых выражений сейчас значения не имеет и в данной статье не рассматривается — они нужны в основном для иллюстрации кода. Надо отметить еще такую особенность: в генерации блока elif-else нужно отслеживать появление else и прекращать генерацию, — в противном случае else может появиться перед elif, что естественно вызовет ошибку. Для этой цели используется флаг fin_else_flag.
Начнем рассмотрение с основной функции генерации.
def if_gen(exp_list, var_list, if_str, offset_koeff, fin_else_flag, prob_list):
choice_list = [exp_list, var_list]
base_offset = ' '
# основная структурная развилка
prob_list[0],prob_list[1],sol = make_solution(prob_list[0],prob_list[1])
# if + блок действия (1 вариант в схеме)
if sol == 0:
# генерим блок действия со смещением+3
action_str = action_str_gen(choice_list, offset_koeff+3, prob_list)
return(base_offset*offset_koeff+'if '+ if_sub(exp_list,var_list, sign, prob_list) +':\n' + action_str, offset_koeff, fin_else_flag, prob_list)
# if + elif/else (2 вариант в схеме)
elif sol == -1:
if_str= base_offset*offset_koeff+'if '+ if_sub(exp_list,var_list, sign, prob_list) +':\n' + action_str_gen(choice_list, offset_koeff+3, prob_list) # if [..]:
# развилка elif/else
prob_list[2],prob_list[3],sol2=make_solution(prob_list[2],prob_list[3])
if sol2!=0:
ee_string='elif'
else:
ee_string='else'
# генерация блока elif/else
if_str, offset_koeff, fin_else_flag, prob_list = elif_else_block(ee_string, offset_koeff, exp_list, var_list, sign, if_str, choice_list, fin_else_flag, prob_list)
return(if_str, offset_koeff, fin_else_flag, prob_list)
# if + if(рекурсия) (3 вариант в схеме)
else:
if_str= base_offset*offset_koeff+'if '+ if_sub(exp_list,var_list, sign, prob_list) +':\n' # if [..]:
# развилка if/if+блок действия
prob_list[4],prob_list[5],sol = make_solution(prob_list[4],prob_list[5])
if sol==0:
# генерим блок действия со смещением+3
if_str+=action_str_gen(choice_list, offset_koeff+3, prob_list)
# сохраняем смещение
wall_offset = offset_koeff
if_rek, offset_koeff, fin_else_flag, prob_list = if_gen(exp_list, var_list, if_str, offset_koeff+3, fin_else_flag, prob_list) # рекурсия if+if
# прицепляем сгенерированный рекурсивный кусок
if_str+=if_rek
# развилка блок elif-else/блок действия
prob_list[4],prob_list[5],sol2=make_solution(prob_list[4],prob_list[5])
if sol2!=0:
prob_list[2],prob_list[3],sol3=make_solution(prob_list[2],prob_list[3])
if sol3!=0:
ee_string='elif'
else:
ee_string='else'
if_str, offset_koeff, fin_else_flag, prob_list = elif_else_block(ee_string, wall_offset, exp_list, var_list, sign, if_str, choice_list, fin_else_flag, prob_list)
else:
# генерим блок действия со смещением+3
if_str+=action_str_gen(choice_list, offset_koeff+3, prob_list)
return(if_str, offset_koeff,fin_else_flag, prob_list)
Функция принимает помимо списков с «мясом» для генерации (exp_list, var_list) также if_str — это строка, куда поочередно собирается генерируемый код. Принимается она здесь по причине того, что сама функция if_gen может рекурсивно вызываться, и кусок кода, сгенерированный ранее, желательно бы не терять.
Параметр offset_koeff — это коэффициент смещения, который является множителем для строки с одним пробелом (base_offset) и, соответственно, именно он и отвечает за горизонтальные смещения блоков кода.
Про fin_else_flag мы говорили выше, здесь он просто передается в функцию, которая отвечает за генерацию if + elif/else (см.ниже).
Ну и есть еще параметр — prob_list, который представляет из себя лист с 10-ю вероятностями (5 парами вероятностей)
prob_list = [0.5 for y in range(0,10)]
и используется функцией make_solution так, как мы обсуждали выше: в нее передается та или иная пара вероятностей из него, соответствующая типу развилки (например, основная структурная развилка использует первые 2 вероятности в листе: prob_list[0] и prob_list[1]). Результаты изменений вероятностей в этом листе, как пример, можно видеть на следующем рисунке.
Вероятности в этом списке меняются от генерации к генерации, если при очередной генерации соответствующий кусок кода попадал на исполнение.
В самой функции в начале инициализирован вложенный список choice_list — он нужен для удобной рандомной генерации выражений из «мяса», и базовое смещение base_offset = ' ' в один пробел.
После этого идет основная развилка, которая через функцию make_solution получает решение в переменную sol. Sol принимает одно из трёх значений (0,-1,1) и обуславливает, следовательно, по какой схеме будет строиться конструкция.
В первом варианте реализуется самая простой вариант if+[..]. Ответ формируется в виде строки со текущим смещением (оно не обязательно равно 0!), строка «if», рандомное условие, которое генерируется функцией if_sub (будет рассмотрена дальше), перевод каретки и генерация блока действия с помощью функции action_str(см.ниже). В результате мы получаем нечто вроде:
if ((a+b)==(b)):
b=b
a=b-a
a=a
Второй вариант отвечает за генерацию такого типа: if [..]+elif/else-блок (вариант 2 в схемах). Сначала там формируется аналогично строка if+[..], затем идет развилка elif/else, которая решает будет ли генерироваться блок elif-else, просто if-elif или if-else (функция elif_else_block — см. ниже). Результаты могут быть разными. Например:
if ((a+b)==(a)):
b=a+b
elif ((b//a)==(a)):
None
elif ((a+b)<=(a)):
a=b//a
else:
if ((b)<=(a)):
a=b-a
b=a
if ((a)==(b-a)):
b=b-a
b=b
a=b
a=b-a
elif ((b)>(b-a))and((a)<(b-a)):
if ((b//a)<(a)):
b=b-a
elif ((a+b)<(b-a))and((b)<(a+b))or((a+b)==(a+b)):
b=b
a=b-a
elif ((a)>(b-a)):
None
if ((b)<=(b-a))or((a+b)>=(b)):
a=a
b=b
elif ((b)<=(b)):
if ((a)>=(b)):
a=a+b
a=b
elif ((b)>=(a)):
a=b-a
a=a
if ((a)>=(b))and((b//a)==(a))and((b//a)!=(b)):
b=b-a
else:
a=b//a
if ((b//a)<(b-a)):
a=b
a=b-a
else:
if ((a)==(b)):
a=a
a=b//a
b=b
b=a+b
b=a
else:
None
Третий вариант реализует рекурсию с самого начала (вариант 3 в схемах), т.е. рождает ветвление вида:
if ((a)==(a)):
if ((a+b)<(b)):
или
if ((b-a)<=(a)):
a=a
if ((b-a)==(b)):
a=a
a=a
Сначала идет формирование строки if (аналогично), потом появляется развилка, которая решает, вставлять ли дальше блок действия или нет, после чего сохраняется смещение и вызывается рекурсия. Смещение нужно сохранить, чтобы после того, как отработает рекурсия и вернет кусок кода, можно было добавить еще блок elif-else по тому же смещению, как и первоначальная строчка с if. Здесь видно, как elif и else в ветвлении стоят по одному смещению со своим «родным» if.
if ((b-a)==(b)):
if ((a)>(a+b)):
if ((b)==(b-a)):
b=b
a=a
elif ((b)>(b)):
None
else:
None
b=a
b=b
Дальше идет развилка elif-else-блок/блок действия, которая решает добавлять ли после рекурсии блок действия или elif-else блок. Если решено добавлять блок elif-else, то там аналогично вышеописанному случаю в схеме 2 выбирается elif или else.
Здесь же надо обратить внимание на то, что рекурсия вызывается со смещением offset+3, чтобы сдвинуть вправо на шаг генерируемый код, а блок elif-else вызывается со смещением wall_offset, чтобы этот блок после рекурсии не поехал вправо, а остался с «родным» смещением изначального if.
Результаты могут быть довольно разными: от простых до сложных, но появление рекурсии сразу же выдает наиболее витиеватые ветвления.
if ((b-a)>(a+b))and((b)<(a+b)):
if ((b-a)<=(a+b)):
b=b//a
elif ((b)!=(a)):
a=b-a
else:
if ((a+b)!=(b-a)):
a=a
if ((b)<(b-a)):
if ((a+b)==(b-a))and((b-a)<(a+b))and((b-a)==(a))and((a)>(b//a))or((a+b)>(b//a)):
if ((b)>=(b-a)):
a=b
b=b
if ((b)>(b)):
a=a+b
b=a+b
a=a
b=a+b
b=b//a
b=a
else:
b=a+b
a=b
a=b
elif ((a)<(b-a)):
a=b//a
a=b-a
if ((a)>=(b-a))or((a)>=(a))or((b)<=(b)):
a=a
a=a
elif ((a)==(a))and((b)>(b-a)):
a=b//a
if ((a)<(b)):
if ((a+b)==(b-a)):
a=a
if ((a)!=(b//a)):
if ((b//a)!=(a))and((b-a)>=(b)):
a=b
else:
None
a=b//a
else:
b=b
b=a+b
if ((b-a)<=(b//a)):
a=b
a=b
a=a+b
else:
a=a+b
if ((b-a)>=(a)):
a=b
if ((b-a)==(a))or((b)!=(b//a)):
a=b-a
a=a
a=a
a=b//a
a=a+b
b=a
Теперь посмотрим на функцию elif_else_block, которая отвечает за формирование блока elif-else и вызывается из основной функции if_gen.
def elif_else_block(ee_string, offset_koeff, exp_list, var_list, sign, if_str, choice_list, fin_else_flag, prob_list):
if ee_string=='elif':
sol3 = 9
# сохраняем смещение
wall_offset = offset_koeff
# генерация elif в цикле
while sol3!=0 and fin_else_flag!=1:
temp_str, offset_koeff, fin_else_flag, prob_list=elif_else('elif', wall_offset, exp_list, var_list, sign, if_str, choice_list, fin_else_flag, prob_list)
if_str+=temp_str
prob_list[6],prob_list[7],sol3 = make_solution(prob_list[6],prob_list[7])
# развилка - добавлять ли else к группе elif?
prob_list[2],prob_list[3],sol = make_solution(prob_list[2],prob_list[3])
if sol!=0:
# добавляем else, значит ставим флаг
fin_else_flag=1
temp_str,offset_koeff, fin_else_flag, prob_list=elif_else('else', wall_offset, exp_list, var_list, sign, if_str, choice_list, fin_else_flag, prob_list)
if_str+=temp_str
return(if_str,offset_koeff, fin_else_flag, prob_list)
# генерация else
else:
temp_str,offset_koeff, fin_else_flag, prob_list=elif_else('else', offset_koeff, exp_list, var_list, sign, if_str, choice_list, fin_else_flag, prob_list)
if_str+=temp_str
return(if_str, offset_koeff, fin_else_flag, prob_list)
Данная функция решает, добавлять ли в код блок elif или elif/else. Ставить ли просто else она сама не решает, а зависит от входящего значения ee_string, которое она получает из основной функции if_gen. Сначала идет генерация блока elif в цикле while, где проверяются 2 условия: вероятностное — от него зависит количество самих elif в блоке и флаг fin_else_flag, который, если вдруг включается, то это означает, что до этого присоединялся else, и следовательно из цикла надо выходить.
Решение присоединять ли к блоку elif еще и else решается развилкой с помощью всё той же функции make_solution, причем, если присоединяется else, то сразу же включается флаг fin_else_flag, который прекращает генерацию блока.
Непосредственное же присоединение elif и else осуществляется функцией elif_else (см.ниже). Здесь надо обратить внимание, что при генерации блока elif (а также при присоединении к нему else) используется смещение wall_offset, чтобы ровно выстраивать блок в целом.
Теперь рассмотрим функцию elif_else .
<b>def elif_else(ee_string, offset_koeff, exp_list, var_list, sign, if_str, choice_list, fin_else_flag, prob_list):
ee_str = ''
# формирование строки else: или elif [..]:
if ee_string=='else':
ee_str += ' '*offset_koeff+ee_string + ':\n'
elif ee_string=='elif':
ee_str += ' '*offset_koeff+ee_string+' '+if_sub(exp_list, var_list, sign, prob_list) + ':\n'
# развилка блок действия-None / блок действия+рекурсия
prob_list[2],prob_list[3],sol = make_solution(prob_list[2],prob_list[3])
if sol!=0:
prob_list[6],prob_list[7],sol2 = make_solution(prob_list[6],prob_list[7])
if sol2!=0:
# блок действия
ee_str+=action_str_gen(choice_list,offset_koeff+3, prob_list)
else:
# None
ee_str+=' '*(offset_koeff+3)+'None\n'
return(ee_str, offset_koeff, fin_else_flag, prob_list)
else:
# подразвилка блок действия
prob_list[6],prob_list[7],sol2 = make_solution(prob_list[6],prob_list[7])
if sol2==0:
# блок действия
ee_str+=action_str_gen(choice_list,offset_koeff+3, prob_list)
# рекурсия if_gen
if_str, offset_koeff, fin_else_flag, prob_list = if_gen(exp_list, var_list, if_str, offset_koeff+3, fin_else_flag, prob_list)
ee_str+=if_str
return(ee_str, offset_koeff, fin_else_flag, prob_list)
Функция отвечает за формирование самой строки elif или else, а также за последующую генерацию после этих строк блоков действий или рекурсии. Она также принимает переменную ee_string, которая содержит или elif, или else, и формирует соответствующую строку. Затем идет развилка, где определяется, что будет идти дальше: (блок действия или None), или (блок действия или блок действия+рекурсия). Внутри этой развилки есть разделение, соответственно на две подразвилки, и в каждом случае вызывается функция make_solution с соответствующими параметрами для принятия решения.
Надо отметить, что когда в коде встречается if sol!=0
, это значит, что мы намеренно даем преимущество одной части кода над другой, ибо если sol!=0, значит она равняется или -1, или 1, и следовательно другой кусок кода будет исполняться реже (только когда sol ==0). Это используется, в частности в функции elif_else_block, где нам выгоднее давать формироваться большему количеству elif в блоке, а не давать равную вероятность elif и else. Или, например, в функции elif_else мы даём преимущество варианту, когда формируется блок действия или None нежели, чем идет рекурсия — в противном случае ветвления могут разрастаться до совсем уже неприличных размеров.
Нам осталось рассмотреть только функции, отвечающие за рандомную генерацию выражений в условиях и блоках действий. Как я и говорил выше, на данном этапе они не играют решающей роли и введены здесь, чтобы в целом показать, как будет выглядеть конечный сгенерированный код. Но так как в генераторе они используются, мы их кратко посмотрим.
Функция, отвечающая за генерацию блока действия action_str.
def action_str_gen(choice_list, offset_koeff, prob_list):
sol = 9
curr_offset = ' '*offset_koeff
act_str = ''
while sol!=0:
act_str+= curr_offset+rand(rand(choice_list[1]))+'='+rand(rand(choice_list))+'\n'
prob_list[6],prob_list[7],sol = make_solution(prob_list[6],prob_list[7])
return(act_str)
Здесь всё достаточно просто: из вложенного списка choise_list, который, как мы помним, состоит из var_list (список переменных) и exp_list (список выражений), эта функция составляет одну или несколько строк такого вида: a = a + b или b = b. Т.е. либо переменной присваивается выражение, либо другая переменная (включая себя саму). Функция rand осуществляют случайную выборку элемента из списка и нужна тут исключительно для того, чтобы не плодить монстроузные строки.
def rand(t_list):
return(t_list[random.randint(0,len(t_list)-1)])
Функция генерации выражений if_sub для условий выглядит побольше.
def if_sub(exp_list, var_list, sign, prob_list):
sub_str = ''
sol = 9
choice_list = [exp_list, var_list]
flag = 0
while sol!=0:
prob_list[6],prob_list[7],sol = make_solution(prob_list[6],prob_list[7])
sub_str+='(('+rand(rand(choice_list))+')'+rand(sign2)+'('+rand(rand(choice_list))+'))'
if flag == 1 and sol==1:
sub_str+=')'
flag=0
or_and_exp = or_and(prob_list)
if len(or_and_exp):
sub_str+=or_and_exp
else:
break
prob_list[6],prob_list[7],sol2 = make_solution(prob_list[6],prob_list[7])
if sol2 == 1 and (sub_str[-1]=='D' or sub_str[-1]=='R') and flag == 0:
sub_str+='('
flag = 1
if sub_str[-1] == '(':
if sub_str[-2]=='d':
sub_str=sub_str[0:-4]
elif sub_str[-2]=='r':
sub_str=sub_str[0:-3]
else:
sub_str=sub_str[0:-1]
elif sub_str[-1]=='d':
sub_str=sub_str[0:-3]
elif sub_str[-1]=='r':
sub_str=sub_str[0:-2]
else:
None
if flag == 1:
sub_str+=')'
return(sub_str)
else:
return(sub_str)
Она генерирует выражения по типу: ((a)>=(b-a))or((a)>=(a))or((b)<=(b)). При этом как в левой, так и в правой части могут быть различные варианты и стоять как отдельные переменные, так и выражения или их группы. Здесь используются также логические операторы or и and, которые выбираются для удобства с помощью функции or_and_exp.
def or_and(prob_list):
prob_list[8],prob_list[9],sol = make_solution(prob_list[8],prob_list[9])
if sol==-1:
return('and')
elif sol==1:
return('or')
else:
return('')
Остальная часть функции if_sub отрезает от выражений лишние хвосты и добавляет, когда нужно, закрывающие скобки, — рассматривать здесь эти танцы с бубнами, я думаю, нецелесообразно.
Ну вот собственно и всё. Запустить генератор можно, например, так:
var_list = ['a','b']
exp_list = ['a+b','b-a', 'b//a']
sign = ['+','-','/','*','//']
sign2 = ['>','<','==','>=','<=','!=']
a = 3
b = 2
prob_list = [0.5 for y in range(0,10)]
while True:
if_str = ''
if_str, offset_koeff, fin_else_flag, prob_list = if_gen(exp_list, var_list, if_str, 0,0, prob_list)
try:
exec(compile(if_str,'gen','exec'))
print(if_str)
input()
except ZeroDivisionError:
None
except:
print('error')
print(if_str)
input()
Сначала входные данные, включая лист с вероятностями prob_list, затем в бесконечном цикле вызов основной функции if_gen и запуск полученной сгенерированной строки на исполнение. Стоит обрабатывать отдельно ZeroDivisionError, т.к. деление на ноль при таком рандомном построении выражений встречается весьма часто. После запуска достаточно жать Enter для появления очередной генерации. Чаще всего они будут достаточно простыми, но нередко встречаются и разветвленные и даже очень разветвленные. Ну и import random в начало тоже было бы неплохо вставить;) Для тех, кто не хочет посмотреть собирать всё руками, можно скачать файл с Github (файл if_gen.py).
В заключение я хочу сказать, что представленный код тестировался мною на сотнях тысячах генераций без ошибок, при этом он демонстрировал всю ту палитру схем if-elif-else, которую я и хотел в конце концов увидеть. Один раз я по ошибке дал в одной части кода слишком большую вероятность рекурсии и у меня вышла генерация в 52 000 (!) строк и при этом она была рабочей (хотя комп подвис секунд на 30). Это также свидетельствует о надежности алгоритма.
Наверное, можно было где-то написать лаконичнее, где-то пооптимизировать, по-другому скомпоновать основную функцию, но главное, что этот код работает и выдает примерно 250 генераций в секунду, что, на мой взгляд, вполне приемлемо.
Я никогда не рассматривал этот код как самодостаточный — он всего лишь модуль будущего цифрового организма и написан в исследовательских целях, поэтому каких-либо практических применений вряд ли имеет. Вместе с тем, я не несу ответственности за какие-либо последствия при любом использовании кем-либо вышеприведенного кода и призываю всех ножом для резки хлеба резать всё-таки хлеб, а не что-то другое.
В следующей статье мы будем рассматривать второй модуль, который будет отвечать за рандомное формирование опыта. Эта тема обещает быть куда интересней, чем генератор if, и я обязательно выложу результаты, как только они у меня будут.