Génération de branches aléatoires en Python

image

Rappelant Dawkins, l'idée principale peut être exprimée comme suit: si vous gardez la tornade sur la poubelle pendant longtemps , alors un Boeing 747 peut s'assembler. L'émergence d'une structure du chaos par un durik: triant et recombinant tout d'affilée, de tous les processus dénués de sens et désordonnés, on peut en voir des assez significatifs et ordonnés. Si de tels processus sont fixes et répétés d'une manière ou d'une autre, alors le système, qui était hier un mouvement brownien, commence aujourd'hui à ressembler à un comportement mis en place par une main invisible et à effectuer des actions significatives de notre point de vue. En même temps, il n'y a aucune main. Elle s'est installée.

Pour m'en assurer à nouveau, je m'efforce d'écrire une sorte de vie numérique qui, hors du chaos et sans instructions inutiles d'une personne, pourra générer de façon aléatoire une logique pour elle-même et exister sur elle dans son habitat naturel - le système d'exploitation. Oui, en cela, il y a probablement une différence avec de nombreux programmes de la direction de la «vie artificielle», qui «vivent» dans les corrals, produisent des «prédateurs» et des «herbivores» et coexistent dans des domaines artificiels avec la «nourriture» et entre eux. Aucun de ces programmes n'interagit avec les objets système (processus, fichiers, etc.), ce qui signifie que le code ne vit pas vraiment. De plus, ce code, d'une manière ou d'une autre, effectue toujours une sorte de tâche dont une personne a besoin et dont la portée est très limitée à cause de cela.

Pour implémenter du code avec une grande liberté d'action dans le système d'exploitation, qui en même temps ne serait pas simplement un ensemble chaotique d'instructions exécutables, un modèle est apparu qui se compose de 3 modules.

  1. Le module de génération aléatoire du code exécutable principal
  2. Module d'éducation aléatoire
  3. Le module "vision par ordinateur" des objets OS

Dans cet article, nous parlerons du premier module, qui jusqu'à présent n'est que la génération de branchements aléatoires, c'est-à-dire constructions comme "if-elif-else". Pourquoi se ramifier? Parce que, dans l'ensemble, la vie de tout organisme vivant se compose de réactions conditionnées: tout ce que nous faisons est une réponse à une information perçue. Les cellules se divisent si certaines conditions se produisent, la victime tente de s'échapper s'il voit un prédateur plus fort, et s'il est plus faible, il peut essayer de l'attaquer, les cafards se dispersent si la lumière s'allume, une personne va manger, s'il a faim, etc. etc. - cette rangée est sans fin. Il n'y a pas d'actions indépendantes et séparées qui ne soient conditionnées par rien. Par conséquent, le comportement des organismes vivants en particulier est décrit comme une réaction à la condition: SI [quelque chose] ALORS [quelque chose]. Nous essayons de générer ce comportement.

Pourquoi au hasard? Afin de laisser le code l'opportunité maximale d'agir indépendamment et d'éloigner autant que possible la personne (programmeur) de ce processus (idéalement complètement exclue). Ce dernier est le plus difficile pour le programmeur, car la programmation standard, à laquelle tout le monde est habitué, ressemble à un dur entraînement des animaux, qui doit effectuer exactement ce que le programmeur indique, exactement comme il l'indique quand il l'indique. Ici, la situation est inverse: le code généré final doit agir de manière à être aussi imprévisible pour le créateur de son générateur.

Avant de passer aux diagrammes et au code du générateur, il est nécessaire de s'attarder sur la fonction de prise de décision, qui sert de conducteur, permettant d'exécuter l'une ou l'autre partie du code. J'ai écrit à son sujet plus tôtici . J'ai ensuite été invité à décrire l'idée de l'apprentissage par renforcement et le jeu de John Conway, intitulé «Life». Il se pourrait bien que je n'ai rien contre l'utilisation de ce qui a déjà été développé ou ouvertement. Au final, tout ce qui est nouveau est une synthèse du déjà connu, et j'ai moi-même admis avoir adopté l'idée de prioriser les flux, qui est utilisée dans Windows. Ici, elle est très appropriée.

Actuellement, la fonction mentionnée a été légèrement transformée:

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 entrée, il prend 2 probabilités (par défaut au départ elles sont toutes deux égales à 0,5), après quoi il vérifie leur fonctionnement un par un. La probabilité déclenchée diminue d'elle-même de 1% et en même temps augmente l'autre de 1%. Par conséquent, chaque fois que la probabilité fonctionne, elle diminue et l'autre augmente. En conséquence, aucune probabilité n'obtient trop d'avantage sur une autre, et ils s'auto-équilibrent, formant une distribution normale centrée sur 0,5 et avec un quartier de travail ne dépassant pas + -10%, ce qui distingue cette fonction du hasard standard, où la probabilité dans notre cas Il serait toujours égal à 0,5 et ne dépendrait pas des calculs précédents.

Au sens figuré, il s'agit d'un pendule de probabilité de faible amplitude. Si la première probabilité a fonctionné et la seconde n'a pas fonctionné, elle renvoie 1, sinon -1 est renvoyée, et si les deux ont fonctionné ou n'ont pas fonctionné, 0. Ainsi, la fonction make_solution pour 2 probabilités entrantes renvoie l'une des 3 actions possibles, donnant un équilibre solution fourchue avec 3 options de continuation possibles. À l'avenir, cette fonction est susceptible d'être universelle et pourra prendre un nombre indéfini de probabilités, car la variation au niveau des fourches peut être supérieure à 3, mais dans le cas du générateur if-elif-else, trois options de poursuite suffisent largement.

Il convient également de noter ici que dans le code, il existe différentes, pour ainsi dire, des fourches typiques. Par exemple, comme on le verra ci-dessous, dans la fonction principale du générateur, il y a une fourchette dans laquelle il y a un choix de schéma pour construire une branche, dont il n'y en a que 3, mais d'autres cas sont également présents dans le code: insérer un bloc d'action ou démarrer une récursivité, combien de lignes d'action doivent être générées, combien compliqué cela devrait être ligne avec la condition, mettre ou ou et, elif ou autre.

Je crois que le pendule probabiliste, dont nous avons parlé ci-dessus, devrait être défini pour chaque type d'action: alors la fourche n'est équilibrée que sur la base de ce qui s'est passé plus tôt sur cette fourchette, et pas dans d'autres parties du code. Ceux. lors du choix de la structure générale de branchement, nous avons notre propre paire de probabilités, et à l'intérieur, lorsque ses éléments sont construits, une autre.

Bien sûr, vous pouvez équilibrer toutes les actions avec une seule paire, mais la probabilité à chaque fourche sera alors très difficile et dépendra de toutes les actions précédentes à d'autres jonctions. Le caractère aléatoire d'un tel design sera encore plus élevé, mais pour l'instant je suis personnellement enclin au premier schéma, car j'aime le design où d'autres petits se balancent dans le cadre d'un grand pendule oscillant, c'est-à-dire des soldes plus petits naissent dans un seul grand équilibre. De plus, dans le deuxième schéma, le caractère aléatoire est également plus que suffisant.

Lors de l'écriture du générateur de branche, il était nécessaire de créer non seulement un code exploitable qui produit des générations sans erreur, mais aussi un code qui pourraitgénérer le maximum de constructions possibles de if-elif-else, mais il n'y a pas 2 ou 3 de ces options possibles. Considérons, par exemple, les schémas possibles suivants.

image

Par l'icône [..] dans les schémas, j'entends un ensemble d'expressions pour une condition ou un bloc d'actions aléatoires. Le schéma le plus élémentaire est 1, où la condition va simplement, et ensuite le bloc d'action. 2a et 2b sont si des variations avec un elif ou un autre. Dans l'option 2c, si vient déjà en combinaison avec plusieurs elif sans autre. Et enfin, dans l'option 2d, le schéma le plus général est présenté, où if contient plusieurs elif et 1 else.

Tout serait simple sans la nécessité de construire des succursales illimitées. Après chaque if, elif ou else, la récursivité peut être appelée, qui à son tour peut également récurser davantage et produire de nouveaux blocs elif-else vers la «droite». Regardons le schéma des options possibles.

image

Les modes de réalisation 2e et 2f montrent des cas spéciaux simples d'une telle ramification récursive lorsque la récursivité est appelée soit après un seul elif, soit après un seul autre. L'option 2g décrit le cas le plus complexe et le plus général d'une telle récursivité, quand après chaque elif il peut y avoir un bloc d'action + récursion (ou immédiatement récursivité), et la même chose peut se produire après autre chose.

Il existe encore des variations lorsque la récursivité se produit immédiatement après if ou après if et un bloc d'action.

image

Cela se voit dans les options 3a et 3b. L'option 3c présente un tel schéma sous sa forme la plus générale.

Cela ne veut pas dire que les schémas ci-dessus couvrent toutes les options possibles pour la construction de branches, mais même sous cette forme, le code final donne facilement naissance à des branches de 150 lignes, allant «à droite» en 10 à 15 étapes. Dans tous les cas, compliquer le schéma si nécessaire n'est pas difficile.

Vous pouvez regarder un exemple d' une telle génération pour vous assurer que les branches peuvent être très diverses.

image

Vous n'avez pas besoin de prêter attention à la composition des expressions conditionnelles et des blocs d'action - pour la simplicité visuelle, ils sont générés à partir uniquement de combinaisons de deux variables, 3 expressions et un petit nombre de signes arithmétiques et logiques. Une discussion sur la véritable «viande» pour la recombinaison est au-delà de la portée de cet article (cela sera discuté dans la discussion de 3 modules).

Avant de procéder à un examen direct du code du générateur, il est nécessaire de se rappeler que les blocs générés doivent être décalés horizontalement vers la droite, s'il est elif, sinon, si des blocs de récursivité ou d'action, et aussi "revenir" vers la gauche après la fin de la branche. De plus, étant donné que Python est très pointilleux sur les retraits horizontaux, il est souhaitable de rendre l'étape identique (dans notre cas, l'étape est 3).

Le diagramme suivant illustre comment les déplacements sont décalés.

image

La chose la plus importante ici est que les déplacements avec l'approfondissement des branches se déplacent toujours vers la droite. Cependant, si nous avons, par exemple, un bloc elif-else dans lequel il y a plusieurs elif ou une seule paire elif-else, alors il est nécessaire de "retourner" le chariot qui flottait vers la droite, de sorte que le prochain elif (ou bien) commence par mêmes décalages que le précédent du bloc. Pour ce faire, vous devez enregistrer le décalage d'origine ( wall_offset) et après la fin de la génération de branche (par exemple, ramification complète d'un elif), restaurez-la. Cela garantit que les éléments elif, else dans le bloc sont «les uns sur les autres» uniformément. De plus, chaque nouveau bloc a son propre déplacement. La même astuce fournit l'harmonie dans la construction globale if-elif-else (y compris les récursions).

Passons maintenant au code. Le code avec un volume total d'environ 200 lignes se compose de 8 fonctions, dont une que nous avons examinée ci-dessus. En raison de la récursivité et d'un grand nombre de paramètres transmis aux fonctions, il peut être mal lisible par endroits. Pour commencer, je citerai la «viande» très utilisée pour générer des expressions conditionnelles et des blocs d'action.

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

Comme vous pouvez le voir, deux variables sont utilisées: a et b ( var_list ), qui sont initialisées, 3 expressions arithmétiques ( exp_list ), et également deux feuilles avec des signes ( sign, sign2 ). Comme mentionné précédemment, la composition des expressions résultantes n'a plus d'importance maintenant et n'est pas considérée dans cet article - elles sont principalement nécessaires pour illustrer le code. Une particularité supplémentaire doit être notée: lors de la génération du bloc elif-else, vous devez suivre l'apparence de else et arrêter la génération, sinon d'autres peuvent apparaître avant elif, ce qui naturellement provoquera une erreur. Le drapeau fin_else_flag est utilisé à cet effet .

Nous commençons notre examen par la fonction de génération principale.

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)

En plus des listes avec «viande» pour la génération (exp_list, var_list), la fonction accepte également if_str - c'est la ligne où le code généré est collecté à son tour. Elle est acceptée ici parce que la fonction if_gen elle-même peut être appelée récursivement, et il serait conseillé de ne pas perdre le morceau de code généré précédemment.

Le paramètre offset_koeff est le coefficient de décalage, qui est un facteur pour une ligne avec un espace ( base_offset ) et, par conséquent, il est responsable des déplacements horizontaux des blocs de code. Nous avons parlé

de fin_else_flag ci-dessus, ici il est simplement passé à une fonction qui est responsable de générer if + elif / else (voir ci-dessous).

Eh bien, il y a un autre paramètre -prob_list , qui est une feuille avec 10 probabilités (5 paires de probabilités)
prob_list = [0.5 for y in range(0,10)] 
et est utilisé par la fonction make_solution comme nous l'avons vu plus haut: telle ou telle paire de probabilités correspondant au type de fork lui est passée (par exemple, la fork structurelle principale utilise les 2 premières probabilités de la feuille: prob_list [0] et prob_list [1] ). Les résultats des changements de probabilité dans cette feuille, à titre d'exemple, peuvent être vus dans la figure suivante.

image

Les probabilités de cette liste changent de génération en génération si, au cours de la génération suivante, le morceau de code correspondant est exécuté.

Dans la fonction elle-même, la liste imbriquée choice_list est initialisée au début - elle est nécessaire pour la génération aléatoire pratique d'expressions à partir de "meat", et la base offset base_offset = '' dans un espace.

Après cela vient le fork principal, qui, via la fonction make_solution, obtient la solution dans la variable sol. Sol prend l'une des trois valeurs (0, -1,1) et détermine, par conséquent, selon le schéma de construction de la structure.

La première option implémente l'option la plus simple si + [..]. La réponse est formée comme une chaîne avec le décalage actuel (il n'est pas nécessairement égal à 0!), Une chaîne «if», une condition aléatoire qui est générée par la fonction if_sub (qui sera discutée plus tard), le retour chariot et la génération d'un bloc d'action à l'aide de la fonction action_str (voir ci-dessous) . En conséquence, nous obtenons quelque chose comme:

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

La deuxième option est responsable de la génération de ce type: if [..] + elif / else-block (option 2 dans les schémas). Tout d'abord, une ligne if + [..] y est formée, puis un fork elif / else se produit, qui décide si le bloc elif-else sera généré, juste si-elif ou if-else (fonction e lif_else_block - voir ci-dessous). Les résultats peuvent varier. Par exemple:

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 troisième option implémente la récursion depuis le tout début (option 3 dans les schémas), c'est-à-dire donne naissance à une branche de la forme:

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

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

Tout d'abord, la ligne if est formée (de manière similaire), puis une fourchette apparaît, qui décide d'insérer ou non le bloc d'action, après quoi le décalage est enregistré et la récursivité est appelée. L'offset doit être sauvegardé pour qu'après la récursivité et le retour du morceau de code, il soit possible d'ajouter un autre bloc elif-else au même offset que la ligne d'origine avec if. Ici, vous pouvez voir comment elif et autre dans la branche se trouvent au même décalage avec leur "natif" 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

Vient ensuite une fourchette dans le bloc elif-else-block / action, qui décide s'il faut ajouter un bloc d'action ou un bloc elif-else après récursivité. Si vous décidez d'ajouter un bloc elif-else, alors là, comme dans le cas décrit ci-dessus, dans le schéma 2, elif ou else est sélectionné.

Ici, il est nécessaire de faire attention au fait que la récursivité est appelée avec un décalage de + 3 pour décaler le code généré vers la droite d'un pas, et le bloc elif-else est appelé avec un décalage de wall_offset afin que ce bloc n'aille pas à droite après la récursivité, mais reste avec le "natif" le décalage de l'original si.

Les résultats peuvent être très différents: du simple au complexe, mais l'apparition de la récursivité produit immédiatement les branches les plus ornées.

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

Examinons maintenant la fonction elif_else_block , qui est responsable de la formation du bloc elif-else et est appelée à partir de la fonction if_gen principale .

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)

Cette fonction décide d'ajouter un bloc elif ou elif / else au code. Elle ne décide pas s'il faut simplement mettre autre chose, mais dépend de la valeur d'entrée e e_string , qu'elle reçoit de la fonction principale if_gen . Tout d'abord, le bloc elif est généré dans la boucle while , où 2 conditions sont vérifiées: probabiliste - le nombre d'elif dans le bloc et le drapeau fin_else_flag en dépendent , et s'il s'allume soudainement, cela signifie que d'autre était connecté avant cela, et donc vous devez quitter la boucle .

La décision d'attacher else et else au bloc elif est décidée par un fork utilisant la même fonction make_solution , et si else est attaché, le drapeau fin_else_flag est activé immédiatementce qui arrête la génération de blocs.

La jonction directe d'elif et d'autre est effectuée par la fonction elif_else (voir ci-dessous). Ici, il est nécessaire de faire attention à ce que lors de la génération du bloc elif (et également lors de l'attachement à un autre élément ), l'offset wall_offset soit utilisé pour construire en douceur le bloc dans son ensemble.

Considérons maintenant la fonction 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 fonction est responsable de la formation de la ligne elif ou bien elle-même, ainsi que de la génération ultérieure de blocs d'action ou de récursivité après ces lignes. Il prend également une variable ee_string , qui contient soit elif, soit else, et forme la chaîne correspondante. Ensuite, il y a un fork, où il est déterminé ce qui se passera ensuite: (bloc d'action ou Aucun), ou (bloc d'action ou bloc d'action + récursivité). À l'intérieur de cette fourchette, il y a une division, respectivement, en deux sous-fourches, et dans chaque cas, la fonction make_solution est appelée avec les paramètres appropriés pour prendre une décision.

Il convient de noter que lorsqu'il se produit dans le codeif sol!=0, cela signifie que nous donnons intentionnellement un avantage à une partie du code par rapport à une autre, car si sol! = 0, alors il est égal à -1 ou 1, et donc un autre morceau de code sera exécuté moins souvent (uniquement lorsque sol == 0). Ceci est utilisé, en particulier, dans la fonction elif_else_block , où il est plus rentable pour nous de laisser plus d' elifs se former dans le bloc, plutôt que de donner une probabilité égale à elif et autre. Ou, par exemple, dans la fonction elif_else, nous donnons un avantage à l'option quand un bloc d'action ou None est formé plutôt que ce que la récursion vise - sinon les branches peuvent atteindre des tailles complètement indécentes.

Il suffit de considérer les fonctions responsables de la génération aléatoire d'expressions dans des conditions et des blocs d'actions. Comme je l'ai dit ci-dessus, à ce stade, ils ne jouent pas un rôle décisif et sont présentés ici pour montrer généralement à quoi ressemblera le code généré final. Mais puisqu'ils sont utilisés dans le générateur, nous les examinerons brièvement.

La fonction responsable de la génération du bloc d' action 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)

Tout est assez simple ici: à partir de la liste imbriquée choise_list, qui, rappelons-le, se compose de v ar_list (liste de variables) et exp_list (liste d'expressions), cette fonction se compose d'une ou plusieurs lignes de cette forme: a = a + b ou b = b . Ceux. soit une expression est affectée à la variable, soit une autre variable (y compris elle-même). La fonction rand sélectionne au hasard un élément de la liste et n'est nécessaire ici que pour ne pas produire de chaînes monstrueuses.

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

La fonction de génération d'expression if_sub pour les conditions semble plus 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)

Il génère des expressions par type: ((a)> = (ba)) ou ((a)> = (a)) ou ((b) <= (b)) . Dans le même temps, les côtés gauche et droit peuvent avoir différentes options et se présenter comme des variables distinctes, ainsi que des expressions ou leurs groupes. Les opérateurs logiques ou et et sont également utilisés ici , qui sont sélectionnés par commodité à l'aide de la fonction 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('')

Le reste de la fonction if_sub coupe les queues supplémentaires des expressions et ajoute, si nécessaire, des crochets de fermeture - pour considérer ces danses avec des tambourins ici, je pense, est inopportun.

Eh bien voilà tout. Vous pouvez démarrer le générateur, par exemple, comme ceci:

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

D'abord, l'entrée, y compris une prob_list avec probabilités , puis dans une boucle infinie, appelant la fonction principale if_gen et démarrant la chaîne générée générée pour exécution. Il vaut la peine de traiter séparément ZeroDivisionError, car la division par zéro avec une telle construction aléatoire des expressions est très courante. Après le lancement, appuyez simplement sur Entrée pour voir une autre génération. Le plus souvent, ils seront assez simples, mais souvent ramifiés et même très ramifiés. Eh bien, l' import aléatoire au début serait également bien à insérer;) Pour ceux qui ne veulent pas tout voir à la main, vous pouvez télécharger le fichier depuis Github (fichier if_gen.py).

En conclusion, je tiens à dire que le code que j'ai présenté a été testé sur des centaines de milliers de générations sans erreur, alors qu'il démontrait toute la palette des schémas if-elif-else que je voulais enfin voir. Une fois, par erreur, j'ai donné dans une partie du code une probabilité de récursivité trop élevée et j'ai obtenu 52 000 (!) Lignes de génération et en même temps ça fonctionnait (bien que la maquette ait été suspendue pendant 30 secondes). Cela indique également la fiabilité de l'algorithme.

Probablement, il était possible d'écrire de manière plus concise quelque part, d'optimiser quelque part, d'organiser la fonction principale d'une autre manière, mais l'essentiel est que ce code fonctionne et génère environ 250 générations par seconde, ce qui, à mon avis, est tout à fait acceptable.

Je n'ai jamais considéré ce code comme autosuffisant - ce n'est qu'un module du futur organisme numérique et a été écrit à des fins de recherche, il n'a donc pratiquement aucune application pratique. En même temps, je ne suis pas responsable des conséquences pour quiconque utilise le code ci-dessus, et j'exhorte tout le monde à couper du pain avec un couteau pour couper le pain, et pas autre chose.

Dans le prochain article, nous considérerons le deuxième module, qui sera responsable de la formation aléatoire de l'expérience. Ce sujet promet d'être beaucoup plus intéressant que le générateur if, et je posterai certainement les résultats dès que je les aurai.

All Articles