Gerando ramificações aleatórias em Python

imagem

Recordando Dawkins, a idéia principal pode ser expressa da seguinte forma: se você mantiver o tornado sobre o lixo por um longo tempo , um Boeing 747 poderá ser montado. O surgimento de uma estrutura do caos por um durik: separando e recombinando tudo em sequência, de todos os processos sem sentido e desordenados, pode-se ver processos bastante significativos e ordenados. Se tais processos são fixos e repetidos de alguma forma, então o sistema, que ontem foi um movimento browniano, hoje começa a parecer que seu comportamento foi estabelecido por uma mão invisível e que está realizando algumas ações significativas do nosso ponto de vista. Ao mesmo tempo, não há mão alguma. Ela se arrumou.

Para me certificar disso novamente, eu me esforço para escrever algum tipo de vida digital que, do caos e sem instruções desnecessárias de uma pessoa, será capaz de gerar aleatoriamente lógica para si mesma e existir nela em seu habitat natural - o sistema operacional. Sim, provavelmente há uma diferença em muitos programas da direção “Vida Artificial”, que “vivem” em currais, produzem “predadores” e “herbívoros” e coexistem em campos artificiais com “comida” e entre si. Nenhum desses programas interage com objetos do sistema (processos, arquivos etc.), o que significa que o código não está realmente ativo. Além disso, esse código, de uma forma ou de outra, ainda executa algum tipo de tarefa que uma pessoa precisa e tem escopo muito limitado por causa disso.

Para implementar código com um alto grau de liberdade de ação no sistema operacional, que ao mesmo tempo não seria apenas um conjunto caótico de instruções executáveis, apareceu um modelo que consiste em 3 módulos.

  1. O módulo de geração aleatória do código executável principal
  2. Random Education Module
  3. O módulo "visão computacional" dos objetos do SO

Neste artigo, falaremos sobre o primeiro módulo, que até agora é apenas a geração de ramificação aleatória, ou seja, construções como "if-elif-else". Por que ramificação? Porque, em geral, a vida de qualquer organismo vivo consiste em reações condicionadas: tudo o que fazemos é uma resposta à informação percebida. As células se dividem se certas condições ocorrerem, a vítima tenta escapar se vê um predador mais forte e, se estiver mais fraco, pode tentar atacá-lo, as baratas se espalham se a luz acender, uma pessoa vai comer, se está com fome etc. etc. - essa linha é interminável. Não há ações independentes e separadas que não sejam condicionadas por nada. Consequentemente, o comportamento dos organismos vivos, em particular, é descrito como uma reação à condição: SE [algo] ENTÃO [algo]. Estamos tentando gerar esse comportamento.

Por que aleatoriamente? Para deixar o código a oportunidade máxima de agir de forma independente e afastar a pessoa (programador) desse processo o mais longe possível (idealmente, excluir completamente). Este último é o mais difícil para o programador, porque A programação padrão, à qual todos estão acostumados, assemelha-se a um treinamento árduo de animais, que deve executar exatamente o que o programador indica, exatamente como ele indica quando ele indica. Aqui a situação é oposta: o código final gerado deve agir de forma que seja imprevisível para o criador de seu gerador.

Antes de avançarmos para os diagramas e o código do gerador, é necessário considerar a função de tomada de decisão, que é usada como condutor, permitindo que uma ou outra parte do código seja executada. Eu escrevi sobre ela anteriormenteaqui . Foi-me solicitado que descrevesse a ideia do Aprendizado por Reforço e o jogo de John Conway, intitulado "Life". Pode ser que eu não tenha nada contra usar o que já foi desenvolvido ou abertamente. No final, tudo o que é novo é uma síntese do já conhecido, e eu próprio admiti que adotei a idéia de priorizar fluxos, que é usada no Windows. Aqui ela é muito adequada.

Atualmente, a função mencionada foi ligeiramente transformada:

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)

Na entrada, são necessárias 2 probabilidades (por padrão, no início, ambas são iguais a 0,5), após o que verifica a operação uma a uma. A probabilidade desencadeada diminui 1% e, ao mesmo tempo, aumenta a outra 1%. Portanto, cada vez que a probabilidade funciona, ela diminui e a outra aumenta. Como resultado, nenhuma probabilidade obtém muita vantagem sobre a outra, e elas se equilibram, formando uma distribuição normal centrada em 0,5 e com uma vizinhança de trabalho não superior a + -10%, o que distingue essa função do aleatório padrão, onde a probabilidade no nosso caso Seria sempre igual a 0,5 e não dependeria de cálculos anteriores.

Figurativamente falando, é um pêndulo de probabilidade com uma pequena amplitude. Se a primeira probabilidade funcionou e a segunda não funcionou, ele retornou 1, caso contrário -1 será retornado e, se funcionou ou não, 0. Portanto, a função make_solution para 2 probabilidades recebidas retornará uma das 3 ações possíveis, fornecendo uma solução bifurcada com 3 opções de continuação possíveis. No futuro, é provável que essa função seja universal e possa assumir um número indefinido de probabilidades, porque a variação nos garfos pode ser maior que 3, mas no caso do gerador if-elif-else, três opções de continuação são suficientes.

Também deve ser observado aqui que no código existem diferentes, por assim dizer, garfos típicos. Por exemplo, como será visto abaixo, na função principal do gerador, existe uma bifurcação na qual existe uma escolha de um esquema para a construção de uma ramificação, da qual existem apenas 3, mas outros casos também estão presentes no código: insira um bloco de ação ou inicie uma recursão, quantas linhas de ação devem ser geradas, quantas linhas de ação devem ser geradas, quão complicado deve ser alinhar com a condição, colocar ou ou e, elif ou então.

Acredito que o pêndulo probabilístico, sobre o qual falamos acima, deve ser definido para cada tipo de ação: então o garfo é equilibrado apenas com base no que aconteceu anteriormente nesse garfo, e não em outras partes do código. Essa. ao escolher a estrutura geral de ramificação, temos nosso próprio par de probabilidades e, dentro, quando seus elementos são construídos, outro.

Obviamente, você pode equilibrar todas as ações com um par, mas a probabilidade em cada bifurcação será muito difícil e dependerá de todas as ações anteriores em outros cruzamentos. A aleatoriedade de um projeto desse tipo será ainda maior, mas por enquanto estou pessoalmente inclinado ao primeiro esquema, porque gosto do design em que outros pequenos oscilam dentro da estrutura de um grande pêndulo oscilante, ou seja, saldos menores nascem em um grande saldo. Além disso, no segundo esquema, a aleatoriedade também é mais que suficiente.

Ao escrever o gerador de ramificação, era necessário criar não apenas código viável que produz gerações sem erros, mas também código que pudessegere o máximo possível de construções de if-elif-else, mas não existem 2 ou 3. dessas opções possíveis.Considere, por exemplo, os seguintes esquemas possíveis.

imagem

Pelo ícone [...] nos esquemas, quero dizer um conjunto de expressões para uma condição ou um bloco de ações aleatórias. O esquema mais elementar é 1, onde a condição simplesmente segue e depois o bloco de ação. 2a e 2b são se variações com um elif ou outro. Na opção 2c, se já vem em combinação com vários elif sem mais. E finalmente, na opção 2d, o esquema mais geral é apresentado, onde se contém vários elif e 1 mais.

Tudo seria simples se não fosse a necessidade de construir filiais ilimitadas. Depois de cada if, elif ou então, a recursão pode ser chamada, o que por sua vez também pode recuar mais e produzir novos blocos de elif-else para o "direito". Vejamos o esquema de opções possíveis.

imagem

As Modalidades 2e e 2f mostram casos especiais simples de ramificação recursiva quando a recursão é chamada após um único elif ou após outro. A opção 2g descreve o caso mais complexo e geral dessa recursão, quando após cada elif pode haver um bloco de ação + recursão (ou recursão imediata), e a mesma coisa pode acontecer depois.

Ainda existem variações quando a recursão ocorre imediatamente após se ou após se e um bloco de ação.

imagem

Isso é visto nas opções 3a e 3b. A opção 3c mostra esse esquema da forma mais geral.

Isso não quer dizer que os esquemas acima abranjam todas as opções possíveis para a construção de ramificações, mas mesmo nesta forma, o código final facilmente gera ramificações de 150 linhas, indo “para a direita” por 10 a 15 etapas. De qualquer forma, complicar o esquema, se necessário, não é difícil.

Você pode ver um exemplo de uma dessas gerações para garantir que os galhos possam ser muito diversos.

imagem

Você não precisa prestar atenção à composição de expressões condicionais e blocos de ação - por simplicidade visual, eles são gerados a partir de apenas combinações de duas variáveis, 3 expressões e um pequeno número de sinais aritméticos e lógicos. Uma discussão sobre a verdadeira "carne" para recombinação está além do escopo deste artigo (isso será discutido na discussão de 3 módulos).

Antes de prosseguir com uma revisão direta do código do gerador, é necessário lembrar que os blocos gerados devem ser deslocados horizontalmente para a direita, se for elif, caso contrário, se houver recursão ou ação, e também "voltar" para a esquerda após a conclusão da ramificação. Além disso, como o Python é muito exigente quanto aos recuos horizontais, é desejável fazer o passo da mesma forma (no nosso caso, o passo é 3).

O diagrama a seguir ilustra como os deslocamentos são deslocados.

imagem

O mais importante aqui é que os deslocamentos com o aprofundamento do galho são sempre deslocados para a direita. No entanto, se tivermos, por exemplo, um bloco elif-else no qual haja vários elif-elos ou um único par elif-else, será necessário "devolver" a carruagem que flutuava para a direita, para que o próximo elif (ou outro) comece com mesmos deslocamentos que o anterior no bloco. Para fazer isso, você deve salvar o deslocamento original ( wall_offset) e após o final da geração da ramificação (por exemplo, ramificação completa de um elif), restaure-a. Isso garante que os elementos elif e else do bloco estejam "em cima um do outro" uniformemente. Além disso, cada novo bloco tem seu próprio deslocamento. O mesmo truque fornece harmonia na construção geral if-elif-else (incluindo recursões).

Agora vamos ao código. O código com um volume total de cerca de 200 linhas consiste em 8 funções, uma das quais examinamos acima. Devido à recursividade e um grande número de parâmetros passados ​​para funções, pode ser pouco legível em alguns locais. Para começar, citarei a própria "carne" usada para gerar expressões condicionais e blocos de ação.

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

Como você pode ver, duas variáveis ​​são usadas: aeb ( var_list ), que são inicializadas, 3 expressões aritméticas ( exp_list ) e também duas folhas com sinais ( sinal, sinal2 ). Como mencionado anteriormente, a composição das expressões resultantes não importa agora e não é considerada neste artigo - elas são necessárias principalmente para ilustrar o código. Mais uma peculiaridade deve ser observada: na geração do bloco elif-else, é necessário rastrear a aparência do else e interromper a geração; caso contrário, pode aparecer antes do elif, o que naturalmente causará um erro. O sinalizador fin_else_flag é usado para essa finalidade .

Começamos nossa consideração com a principal função de geração.

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)

Além das listas com “carne” para geração (exp_list, var_list), a função também aceita if_str - esta é a linha na qual o código gerado é coletado por sua vez. É aceito aqui porque a função if_gen pode ser chamada recursivamente e é aconselhável não perder o trecho de código gerado anteriormente.

O parâmetro offset_koeff é o coeficiente de deslocamento, que é um fator para uma linha com um espaço ( base_offset ) e, portanto, é responsável pelos deslocamentos horizontais dos blocos de código. Falamos

sobre fin_else_flag acima, aqui é simplesmente passado para uma função que é responsável por gerar if + elif / else (veja abaixo).

Bem, há outro parâmetro -prob_list , que é uma planilha com 10 probabilidades (5 pares de probabilidades)
prob_list = [0.5 for y in range(0,10)] 
e é usado pela função make_solution como discutimos acima: este ou aquele par de probabilidades correspondentes ao tipo de bifurcação é passado para ele (por exemplo, o principal bifurcação estrutural usa as 2 primeiras probabilidades da planilha: prob_list [0] e prob_list [1] ). Os resultados das mudanças de probabilidade nesta planilha, como exemplo, podem ser vistos na figura a seguir.

imagem

As probabilidades nesta lista mudam de geração para geração, se durante a próxima geração o trecho de código correspondente for executado.

Na própria função, a lista de opções aninhada é inicializada no início - é necessária para a geração aleatória conveniente de expressões de “carne” e o deslocamento base base_offset = '' em um espaço.

Depois disso, vem o fork principal, que, através da função make_solution, coloca a solução na variável sol. Sol pega um dos três valores (0, -1,1) e determina, portanto, de acordo com o esquema em que a estrutura será construída.

A primeira opção implementa a opção mais simples se + [..]. A resposta é formada como uma string com o deslocamento atual (não é necessariamente igual a 0!), Uma string "if", uma condição aleatória gerada pela função if_sub (que será discutida mais adiante), retorno de carro e geração de um bloco de ação usando a função action_str (veja abaixo) . Como resultado, obtemos algo como:

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

A segunda opção é responsável por gerar esse tipo: if [..] + elif / else-block (opção 2 nos esquemas). Primeiro, uma linha if + [..] é formada lá, ocorre um fork elif / else, que decide se o bloco elif-else será gerado, apenas if-elif ou if-else (função e lif_else_block - veja abaixo). Os resultados podem variar. Por exemplo:

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

A terceira opção implementa a recursão desde o início (opção 3 nos esquemas), ou seja, dá origem a um ramo do formulário:

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

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

Primeiro, a linha if é formada (da mesma forma), então um fork aparece, que decide se deseja inserir o bloco de ação ou não, após o qual o deslocamento é salvo e a recursão é chamada. O deslocamento deve ser salvo para que, após a recursão ser concluída e o código retornado, seja possível adicionar outro bloco elif-else no mesmo deslocamento que a linha original com if. Aqui você pode ver como elif e o resto do ramo ficam no mesmo deslocamento com o "nativo" deles.

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

Em seguida, vem uma bifurcação no bloco elif-else-block / action, que decide se deve adicionar um bloco de ação ou um bloco elif-else após a recursão. Se você decidir adicionar um bloco elif-else, então, de maneira semelhante ao caso descrito acima, no esquema 2, elif ou então será selecionado.

Aqui é necessário prestar atenção ao fato de que a recursão é chamada com um deslocamento de + 3 para deslocar o código gerado para a direita em uma etapa, e o bloco elif-else é chamado com um deslocamento de wall_offset para que esse bloco não vá para a direita após a recursão, mas permanece com o "nativo" o deslocamento do original se.

Os resultados podem ser bem diferentes: do simples ao complexo, mas a aparência da recursão produz imediatamente os ramos mais ornamentados.

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

Agora, vejamos a função elif_else_block , responsável por formar o bloco elif-else e é chamada a partir da função if_gen principal .

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 função decide se um bloco elif ou elif / else deve ser adicionado ao código. Ela não decide se simplesmente deve colocar outra coisa, mas depende do valor de entrada e e_string , que recebe da função principal if_gen . Primeiro, o bloco elif é gerado no loop while , onde duas condições são verificadas: probabilistic - o número de elif no bloco e o sinalizador fin_else_flag dependem dele e, se ele for ativado repentinamente, significa que outra coisa foi conectada antes disso e, portanto, você precisa sair do loop .

A decisão de anexar else e else ao bloco elif é decidida por uma bifurcação usando a mesma função make_solution e, se mais estiver anexado, o sinalizador fin_else_flag será ativado imediatamenteque interrompe a geração de blocos.

A junção direta de elif e else é realizada pela função elif_else (veja abaixo). Aqui é necessário prestar atenção que, ao gerar o bloco elif (e também ao anexá-lo), o offset wall_offset é usado para construir o bloco como um todo.

Agora considere a função 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)

A função é responsável pela formação do elif ou pela própria linha, bem como pela geração subsequente de blocos de ação ou recursão após essas linhas. Ele também pega uma variável ee_string , que contém elif ou então, e forma a string correspondente. Depois, há uma bifurcação, onde é determinado o que será a seguir: (bloco de ação ou Nenhum) ou (bloco de ação ou bloco de ação + recursão). Dentro deste fork, há uma divisão, respectivamente, em dois sub fork, e em cada caso a função make_solution é chamada com os parâmetros apropriados para tomar uma decisão.

Note-se que quando ocorre no códigoif sol!=0, isso significa que intencionalmente damos uma vantagem a uma parte do código em relação a outra, porque se sol! = 0, então é igual a -1 ou 1 e, portanto, outra parte do código será executada com menos frequência (somente quando sol == 0). Isso é usado, em particular, na função elif_else_block , onde é mais lucrativo permitir que mais elifs se formem no bloco, em vez de dar igual probabilidade a elif e mais. Ou, por exemplo, na função elif_else, damos uma vantagem à opção quando um bloco de ação ou Nenhum é formado, e não o objetivo da recursão - caso contrário, os ramos podem crescer para tamanhos muito indecentes.

Precisamos apenas considerar as funções responsáveis ​​pela geração aleatória de expressões em condições e blocos de ações. Como eu disse acima, nesta fase eles não desempenham um papel decisivo e são introduzidos aqui para mostrar geralmente como será o código final gerado. Mas, como eles são usados ​​no gerador, vamos examiná-los brevemente.

A função responsável por gerar o bloco de ação 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)

Tudo é bem simples aqui: da lista aninhada choise_list, que, como lembramos, consiste em v ar_list (lista de variáveis) e exp_list (lista de expressões), essa função consiste em uma ou mais linhas desta forma: a = a + b ou b = b . Essa. uma expressão é atribuída à variável ou outra variável (incluindo ela mesma). A função rand seleciona aleatoriamente um elemento da lista e é necessária aqui apenas para não produzir seqüências monstruosas.

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

A função de geração de expressão if_sub para condições parece maior.

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)

Ele gera expressões por tipo: ((a)> = (ba)) ou ((a)> = (a)) ou ((b) <= (b)) . Ao mesmo tempo, os lados esquerdo e direito podem ter várias opções e permanecer como variáveis ​​separadas, bem como expressões ou seus grupos. Os operadores lógicos ou e e também são usados ​​aqui , selecionados por conveniência usando a função 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('')

O restante da função if_sub corta as caudas extras das expressões e adiciona, quando necessário, colchetes, para considerar essas danças com pandeiros aqui, eu acho, inconvenientes.

Bom, isso é tudo. Você pode iniciar o gerador, por exemplo, assim:

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

Primeiro, a entrada, incluindo uma prob_list com probabilidades , depois em um loop infinito, chamando a função principal if_gen e iniciando a sequência gerada resultante para execução. Vale a pena processar separadamente o ZeroDivisionError, porque A divisão por zero com uma construção tão aleatória de expressões é muito comum. Após o lançamento, basta pressionar Enter para a próxima geração aparecer. Na maioria das vezes eles serão bastante simples, mas muitas vezes ramificados e até muito ramificados. Bem, importar aleatoriamente no início também seria bom inserir;) Para aqueles que não querem ver a coleta de tudo manualmente, você pode fazer o download do arquivo no Github (arquivo if_gen.py).

Concluindo, quero dizer que o código que apresentei foi testado em centenas de milhares de gerações sem erros, enquanto demonstrava toda a paleta de esquemas de if-elif-else que eu queria ver finalmente. Uma vez, por engano, dei a uma parte do código uma probabilidade muito alta de recursão e obtive 52.000 (!) Linhas de geração e ele estava funcionando (embora o comp tenha suspendido 30 segundos). Isso também indica a confiabilidade do algoritmo.

Provavelmente, foi possível escrever de forma mais concisa em algum lugar, otimizar em algum lugar, compor a função principal de outra maneira, mas o principal é que esse código funcione e gere cerca de 250 gerações por segundo, o que, na minha opinião, é bastante aceitável.

Eu nunca considerei esse código auto-suficiente - é apenas um módulo do futuro organismo digital e foi escrito para fins de pesquisa, por isso dificilmente tem aplicações práticas. Ao mesmo tempo, não sou responsável por nenhuma conseqüência para quem usa o código acima e exorto todos a cortar pão com uma faca para cortar pão, e não outra coisa.

No próximo artigo, consideraremos o segundo módulo, que será responsável pela formação aleatória da experiência. Esse tópico promete ser muito mais interessante do que o gerador if, e eu definitivamente publicarei os resultados assim que os tiver.

All Articles