Generando ramas aleatorias en Python

imagen

Recordando a Dawkins, la idea principal se puede expresar de la siguiente manera: si mantienes el tornado sobre la basura durante mucho tiempo , entonces se puede armar un Boeing 747. El surgimiento de una estructura del caos por un durik: clasificando y recombinando todo en una fila, de todos los procesos sin sentido y desordenados, uno puede ver los bastante significativos y ordenados. Si tales procesos se arreglan y repiten de alguna manera, entonces el sistema, que ayer era un movimiento browniano, hoy comienza a parecer que su comportamiento fue establecido por una mano invisible, y que está haciendo algunas acciones que son significativas desde nuestro punto de vista. Al mismo tiempo, no hay mano en absoluto. Ella se preparó.

Para asegurarme de esto nuevamente, me esfuerzo por escribir algún tipo de vida digital que, fuera del caos y sin instrucciones innecesarias de una persona, pueda generar al azar lógica para sí misma y existir en ella en su hábitat natural: el sistema operativo. Sí, en esto, probablemente, hay una diferencia de muchos programas de la dirección de "Vida Artificial", que "viven" en corrales, producen "depredadores" y "herbívoros", y coexisten en campos artificiales con "alimentos" y entre sí. Ninguno de estos programas interactúa con los objetos del sistema (procesos, archivos, etc.), lo que significa que el código realmente no vive. Además, este código de una forma u otra todavía realiza algún tipo de tarea que una persona necesita y tiene un alcance muy limitado debido a esto.

Para implementar código con un alto grado de libertad de acción en el sistema operativo, que al mismo tiempo no sería solo un conjunto caótico de instrucciones ejecutables, apareció un modelo que consta de 3 módulos.

  1. El módulo de generación aleatoria del código ejecutable principal.
  2. Módulo de educación al azar
  3. El módulo de "visión por computadora" de los objetos del sistema operativo

En este artículo, hablaremos sobre el primer módulo, que hasta ahora es solo la generación de ramificaciones aleatorias, es decir, construcciones como "if-elif-else". ¿Por qué ramificarse? Porque, en general, la vida de cualquier organismo vivo consiste en reacciones condicionadas: todo lo que hacemos es una respuesta a la información percibida. Las células se dividen si ocurren ciertas condiciones, la víctima trata de escapar si ve un depredador más fuerte, y si es más débil, puede intentar atacarlo, las cucarachas se dispersan si se enciende la luz, una persona va a comer, si tiene hambre, etc. etc. - Esta fila es interminable. No hay acciones independientes y separadas que no estén condicionadas por nada. En consecuencia, el comportamiento de los organismos vivos en particular se describe como una reacción a la condición: SI [algo] ENTONCES [algo]. Estamos tratando de generar este comportamiento.

¿Por qué al azar? Para dejar el código, la máxima oportunidad de actuar de forma independiente y alejar a la persona (programador) de este proceso lo más lejos posible (idealmente excluir por completo). Este último es el más difícil para el programador, porque La programación estándar, a la que todos están acostumbrados, se asemeja a un duro entrenamiento de animales, que debe realizar exactamente lo que indica el programador, exactamente como él indica cuando lo indica. Aquí la situación es la opuesta: el código final generado debe actuar de manera que sea tan impredecible para el creador de su generador.

Antes de pasar a los diagramas y al código del generador, es necesario detenerse en la función de toma de decisiones, que se utiliza como conductor, permitiendo que se ejecute una u otra parte del código. Escribí sobre ella antesAquí . Luego se me solicitó que describiera la idea de Reinforcement Learning y el juego de John Conway, titulado "Life". Bien puede ser que no tengo nada en contra de usar lo que ya se ha desarrollado o abiertamente. Al final, todo lo nuevo es una síntesis de lo ya conocido, y yo mismo admití que adopté la idea de priorizar los flujos, que se usa en Windows. Aquí ella es muy adecuada.

Actualmente, la función mencionada se ha transformado ligeramente:

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)

En la entrada, se necesitan 2 probabilidades (de forma predeterminada al principio, ambas son iguales a 0,5), después de lo cual verifica su operación una por una. La probabilidad disparada disminuye en un 1% y al mismo tiempo aumenta la otra en un 1%. Por lo tanto, cada vez que la probabilidad funciona, disminuye y la otra aumenta. Como resultado, ninguna probabilidad obtiene demasiada ventaja sobre otra, y se autoequilibran, formando una distribución normal centrada en 0.5 y con una vecindad de trabajo de no más de + -10%, lo que distingue esta función del azar estándar, donde la probabilidad en nuestro caso Siempre sería igual a 0.5 y no dependería de cálculos previos.

Hablando en sentido figurado, es un péndulo de probabilidad con una pequeña amplitud. Si la primera probabilidad funcionó y la segunda no funcionó, devuelve 1, de lo contrario -1 se devuelve, y si ambas funcionaron o no funcionaron, 0. Por lo tanto, la función make_solution para 2 probabilidades entrantes devuelve una de 3 acciones posibles, dando un balance solución bifurcada con 3 posibles opciones de continuación. En el futuro, es probable que esta función sea universal y pueda tomar un número indefinido de probabilidades, porque la variación en las horquillas puede ser más de 3, pero en el caso del generador if-elif-else, tres opciones para continuar son suficientes.

También debe tenerse en cuenta aquí que en el código hay diferentes, por así decir, horquillas típicas. Por ejemplo, como se verá a continuación, en la función principal del generador hay una bifurcación en la que se puede elegir un esquema para construir una rama, del cual solo hay 3, pero también hay otros casos en el código: inserte un bloque de acción o comience una recursión, cuántas líneas de acción generar, qué tan complejo debería ser línea con la condición, poner o o y, elif o de lo contrario.

Creo que el péndulo probabilístico, del que hablamos anteriormente, debe establecerse para cada tipo de acción: entonces la bifurcación se equilibra solo en función de lo que sucedió anteriormente en esta bifurcación, y no en otras partes del código. Aquellos. Al elegir la estructura de ramificación general, tenemos nuestro propio par de probabilidades, y dentro, cuando se construyen sus elementos, otro.

Por supuesto, puede equilibrar todas las acciones con un par, pero la probabilidad en cada bifurcación será muy difícil y dependerá de todas las acciones anteriores en otros cruces. La aleatoriedad de dicho diseño será aún mayor, pero por ahora personalmente me inclino por el primer esquema, porque me gusta el diseño donde otros pequeños se balancean dentro del marco de un péndulo oscilante grande, es decir. saldos más pequeños nacen en un gran saldo. Además, en el segundo esquema, la aleatoriedad también es más que suficiente.

Al escribir el generador de rama, era necesario crear no solo un código viable que produjera generaciones sin errores, sino también un código que pudieragenerar las construcciones máximas posibles de if-elif-else, pero no hay 2 o 3 de tales opciones posibles Considere, por ejemplo, los siguientes esquemas posibles.

imagen

Por el icono [..] en los esquemas me refiero a un conjunto de expresiones para una condición o un bloque de acciones aleatorias. El esquema más elemental es 1, donde la condición simplemente va, y luego el bloque de acción. 2a y 2b son si hay variaciones con un elif u otro. En la opción 2c, si ya viene en combinación con varios elif sin más. Y finalmente, en la opción 2d, se presenta el esquema más general, donde if contiene varios elif y 1 más.

Todo sería simple si no fuera por la necesidad de construir sucursales ilimitadas. Después de cada if, elif o else, se puede llamar a la recursión, que a su vez también puede repetirse más y producir nuevos bloques elif-else a la "derecha". Veamos el esquema de posibles opciones.

imagen

Las realizaciones 2e y 2f muestran casos especiales simples de tal ramificación recursiva cuando se llama a la recursión después de un solo elif o de otro. La opción 2g describe el caso más complejo y general de dicha recursión, cuando después de cada elif puede haber un bloque de acción + recursión (o recursividad inmediata), y lo mismo puede suceder después de lo contrario.

Todavía hay variaciones cuando la recursión ocurre inmediatamente después de if o after if y un bloque de acción.

imagen

Esto se ve en las opciones 3a y 3b. La opción 3c muestra dicho esquema en la forma más general.

Esto no quiere decir que los esquemas anteriores cubren todas las opciones posibles para construir ramas, pero incluso en esta forma, el código final da lugar fácilmente a ramas de 150 líneas, yendo "a la derecha" por 10-15 pasos. En cualquier caso, complicar el esquema si es necesario no es difícil.

Puede ver un ejemplo de una de esas generaciones para asegurarse de que las ramas pueden ser muy diversas.

imagen

No es necesario prestar atención a la composición de las expresiones condicionales y los bloques de acción; por simplicidad visual, se generan a partir de solo combinaciones de dos variables, 3 expresiones y un pequeño número de signos aritméticos y lógicos. Una discusión sobre la verdadera "carne" para la recombinación está más allá del alcance de este artículo (esto se discutirá en la discusión de 3 módulos).

Antes de proceder a un examen directo del código del generador, es necesario recordar que los bloques generados deben desplazarse horizontalmente hacia la derecha, si es elif, de lo contrario, si son bloques de recursión o acción, y también "regresar" a la izquierda después de que la rama se complete. Además, dado que Python es muy exigente con las sangrías horizontales, es deseable que el paso sea el mismo (en nuestro caso, el paso es 3).

El siguiente diagrama ilustra cómo se desplazan los desplazamientos.

imagen

Lo más importante aquí es que los desplazamientos con la profundización de las ramas siempre se desplazan hacia la derecha. Sin embargo, si tenemos, por ejemplo, un bloque elif-else en el que hay varios elif o un solo par elif-else, entonces es necesario "devolver" el carro que flotó hacia la derecha, de modo que el siguiente elif (o sino) comience con Las mismas compensaciones que la anterior en el bloque. Para hacer esto, debe guardar el desplazamiento original ( wall_offset) y después del final de la generación de sucursales (por ejemplo, la ramificación completa de un elif), restaúrelo. Esto asegura que el elif, de lo contrario, los elementos están en el bloque de manera uniforme "uno encima del otro". Además, cada nuevo bloque tiene su propio desplazamiento. El mismo truco proporciona armonía en la construcción general if-elif-else (incluidas las recursiones).

Ahora pasemos al código. El código con un volumen total de aproximadamente 200 líneas consta de 8 funciones, una de las cuales examinamos anteriormente. Debido a la recursividad y una gran cantidad de parámetros pasados ​​a las funciones, puede ser poco legible en algunos lugares. Para comenzar, citaré la misma "carne" que se usa para generar expresiones condicionales y bloques de acción.

var_list = ['a','b']
exp_list = ['a+b','b-a', 'b//a']
sign = ['+','-','/','*','//']
sign2 = ['>','<','==','>=','<=','!=']
a = 3
b = 2

Como puede ver, se utilizan dos variables: a y b ( var_list ), que se inicializan, 3 expresiones aritméticas ( exp_list ), y también dos hojas con signos ( sign, sign2 ). Como se mencionó anteriormente, la composición de las expresiones resultantes no importa ahora y no se considera en este artículo; se necesitan principalmente para ilustrar el código. Se debe tener en cuenta una peculiaridad más: en la generación del bloque elif-else, debe realizar un seguimiento de la apariencia del else y detener la generación; de lo contrario, puede aparecer otro antes del elif, lo que naturalmente causará un error. La bandera fin_else_flag se usa para este propósito .

Comenzamos nuestra consideración con la función de generación principal.

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)

Además de las listas con "carne" para la generación (exp_list, var_list), la función también acepta if_str : esta es la línea donde el código generado se recopila a su vez. Se acepta aquí porque la función if_gen en sí misma se puede llamar de forma recursiva, y sería aconsejable no perder el fragmento de código generado anteriormente.

El parámetro offset_koeff es el coeficiente de desplazamiento, que es un factor para una línea con un espacio ( base_offset ) y, en consecuencia, es responsable de los desplazamientos horizontales de los bloques de código. Hablamos

sobre fin_else_flag arriba, aquí simplemente se pasa a una función que es responsable de generar if + elif / else (ver más abajo).

Bueno, hay otro parámetro:prob_list , que es una hoja con 10 probabilidades (5 pares de probabilidades)
prob_list = [0.5 for y in range(0,10)] 
y es utilizada por la función make_solution como discutimos anteriormente: se le pasa uno u otro par de probabilidades correspondientes al tipo de bifurcación (por ejemplo, la bifurcación estructural principal usa las 2 primeras probabilidades en la hoja: prob_list [0] y prob_list [1] ). Los resultados de los cambios de probabilidad en esta hoja, como ejemplo, se pueden ver en la siguiente figura.

imagen

Las probabilidades en esta lista cambian de generación en generación, si durante la próxima generación se ejecuta el código correspondiente.

En la función en sí, la lista anidada choice_list se inicializa al principio: es necesaria para la generación aleatoria conveniente de expresiones a partir de "carne", y el desplazamiento base base_offset = '' en un espacio.

Después de eso viene la bifurcación principal, que, a través de la función make_solution, obtiene la solución en la variable sol. Sol toma uno de los tres valores (0, -1.1) y determina, por lo tanto, de acuerdo con qué esquema se construirá la estructura.

La primera opción implementa la opción más simple si + [..]. La respuesta se forma como una cadena con el desplazamiento actual (¡no es necesariamente igual a 0!), Una cadena "if", una condición aleatoria que es generada por la función if_sub (que se discutirá más adelante), retorno de carro y generación de un bloque de acción usando la función action_str (ver más abajo) . Como resultado, obtenemos algo como:

if ((a+b)==(b)):
   b=b
   a=b-a
   a=a

La segunda opción es responsable de generar este tipo: if [..] + elif / else-block (opción 2 en los esquemas). Primero, la línea if + [..] se forma allí, luego se produce la bifurcación elif / else, que decide si se generará el bloque elif-else, solo if-elif o if-else (función lif_else_block - ver más abajo). Los resultados pueden variar. Por ejemplo:

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

La tercera opción implementa la recursividad desde el principio (opción 3 en los esquemas), es decir da lugar a una rama de la forma:

if ((a)==(a)):
   if ((a+b)<(b)):

o
if ((b-a)<=(a)):
   a=a
   if ((b-a)==(b)):
      a=a
      a=a

Primero, se forma la línea if (de manera similar), luego aparece una bifurcación, que decide si se inserta más el bloque de acción o no, después de lo cual se guarda el desplazamiento y se llama a la recursión. El desplazamiento debe guardarse de modo que después de que se complete la recursión y se devuelva el fragmento de código, es posible agregar otro bloque elif-else en el mismo desplazamiento que la línea original con if. Aquí puede ver cómo elif y demás en la rama se encuentran en el mismo desplazamiento con su "nativo" si.

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

Luego viene una bifurcación en el bloque elif-else-block / action, que decide si agregar un bloque de acción o un bloque elif-else después de la recursividad. Si decide agregar un bloque elif-else, entonces, de manera similar al caso descrito anteriormente, en el esquema 2, se selecciona elif o else.

Aquí es necesario prestar atención al hecho de que se llama a la recursión con un desplazamiento de + 3 para desplazar el código generado a la derecha por un paso, y el bloque elif-else se llama con un desplazamiento de wall_offset para que este bloque no vaya a la derecha después de la recursión, sino que permanezca con el "nativo" el desplazamiento del original si.

Los resultados pueden ser bastante diferentes: de simples a complejos, pero la aparición de recurrencia produce inmediatamente las ramas más ornamentadas.

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

Ahora veamos la función elif_else_block , que es responsable de formar el bloque elif-else y se llama desde la función principal 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)

Esta función decide si agregar un bloque elif o elif / else al código. Ella no decide si simplemente quiere decir otra cosa, sino que depende del valor de entrada e e_string , que recibe de la función principal if_gen . Primero, el bloque elif se genera en el ciclo while , donde se verifican 2 condiciones: probabilístico: el número de elif en el bloque y la bandera fin_else_flag dependen de él , y si se enciende repentinamente, significa que más se conectó antes y, por lo tanto, debe salir del ciclo .

La decisión de adjuntar else y else al bloque elif se decide mediante una bifurcación que utiliza la misma función make_solution , y si se adjunta más, el indicador fin_else_flag se activa inmediatamenteque detiene la generación de bloques.

La unión directa de elif y else se realiza mediante la función elif_else (ver más abajo). Aquí es necesario prestar atención a que al generar el bloque elif (y también al adjuntarle otro), el offset wall_offset se usa para construir el bloque sin problemas en su conjunto.

Ahora considere la función 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)

La función es responsable de la formación de la línea elif o de lo contrario, así como de la generación posterior de bloques de acción o recursividad después de estas líneas. También toma una variable ee_string , que contiene elif o else, y forma la cadena correspondiente. Luego hay una bifurcación, donde se determina qué pasará a continuación: (bloque de acción o Ninguno), o (bloque de acción o bloque de acción + recursión). Dentro de esta bifurcación, hay una división, respectivamente, en dos sub-bifurcaciones, y en cada caso la función make_solution se llama con los parámetros apropiados para tomar una decisión.

Cabe señalar que cuando ocurre en el códigoif sol!=0, esto significa que intencionalmente le damos una ventaja a una parte del código sobre otra, porque si sol! = 0, entonces es igual a -1 o 1, y por lo tanto, otro fragmento de código se ejecutará con menos frecuencia (solo cuando sol == 0). Esto se usa, en particular, en la función elif_else_block , donde es más rentable para nosotros dejar que se formen más elifs en el bloque, en lugar de dar la misma probabilidad a elif y demás. O, por ejemplo, en la función elif_else, le damos una ventaja a la opción cuando se forma un bloque de acción o Ninguno en lugar de lo que está buscando la recursión; de lo contrario, las ramas pueden crecer hasta tamaños muy indecentes.

Solo necesitamos considerar las funciones responsables de la generación aleatoria de expresiones en condiciones y bloques de acciones. Como dije anteriormente, en esta etapa no juegan un papel decisivo y se presentan aquí para mostrar en general cómo se verá el código final generado. Pero como se usan en el generador, los veremos brevemente.

La función responsable de generar el bloque de acción 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)

Aquí todo es bastante simple: de la lista anidada choise_list, que, como recordamos, consiste en v ar_list (lista de variables) y exp_list (lista de expresiones), esta función consta de una o más líneas de esta forma: a = a + b o b = b . Aquellos. se asigna una expresión a la variable u otra variable (incluida ella misma). La función rand selecciona aleatoriamente un elemento de la lista y se necesita aquí únicamente para no producir cadenas monstruosas.

def rand(t_list):
    return(t_list[random.randint(0,len(t_list)-1)])

La función de generación de expresiones if_sub para condiciones parece más grande.

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)

Genera expresiones por tipo: ((a)> = (ba)) o ((a)> = (a)) o ((b) <= (b)) . Al mismo tiempo, los lados izquierdo y derecho pueden tener varias opciones y representarse como variables separadas, así como expresiones o sus grupos. Los operadores lógicos o y y también se utilizan aquí , que se seleccionan por conveniencia usando el or_and_exp función .

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('')

El resto de la función if_sub corta las colas adicionales de las expresiones y agrega, cuando es necesario, cerrar los corchetes, para considerar estos bailes con panderetas aquí, creo que es inoportuno.

Bueno eso es todo. Puede iniciar el generador, por ejemplo, así:

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()

Primero, la entrada, que incluye un prob_list con probabilidades , luego en un bucle infinito, llamando a la función principal if_gen e iniciando la cadena generada generada para su ejecución. Vale la pena procesar por separado ZeroDivisionError, porque La división por cero con una construcción de expresiones tan aleatoria es muy común. Después del lanzamiento, simplemente presione Entrar para que aparezca la próxima generación. La mayoría de las veces serán bastante simples, pero a menudo ramificadas e incluso muy ramificadas. Bueno, importar al azar al principio también sería bueno para insertar;) Para aquellos que no quieren ver la recopilación de todo a mano, puede descargar el archivo de Github (archivo if_gen.py).

En conclusión, quiero decir que el código que presenté fue probado en cientos de miles de generaciones sin errores, mientras que demostró toda la paleta de esquemas if-elif-else que quería ver finalmente. Una vez, por error, le di a una parte del código una probabilidad demasiado alta de recurrencia y obtuve 52,000 (!) Líneas de generación y estaba funcionando (aunque la compilación se suspendió por 30 segundos). Esto también indica la fiabilidad del algoritmo.

Probablemente, fue posible escribir de manera más concisa en alguna parte, optimizar en alguna parte, componer la función principal de otra manera, pero lo principal es que este código funciona y genera alrededor de 250 generaciones por segundo, lo cual, en mi opinión, es bastante aceptable.

Nunca consideré este código como autosuficiente: es solo un módulo del futuro organismo digital y fue escrito con fines de investigación, por lo que apenas tiene aplicaciones prácticas. Al mismo tiempo, no soy responsable de las consecuencias de que alguien use el código anterior, e insto a todos a cortar el pan con un cuchillo para cortar pan, y no otra cosa.

En el próximo artículo, consideraremos el segundo módulo, que será responsable de la formación aleatoria de la experiencia. Este tema promete ser mucho más interesante que el generador if, y definitivamente publicaré los resultados tan pronto como los tenga.

All Articles