2. Un peu plus loin avec les tableaux...

Les tableaux sont des objets très utilisés en informatique, mais pas toujours évidents à manipuler, d'autant plus qu'en Python ( et dans d'autres langages ), il y a quelques subtilités dont il faut avoir conscience...

Copie de tableaux

Copie d'une variable

Il arrivera souvent où l'on aura besoin de recopier un tableau dans un autre.

Pour cela, la première idée qui vient à l'esprit est d'utiliser une opération affectation comme on pourrait le faire avec deux variables :



				

→ pas de problème : on a maintenant deux variables indépendantes dont on peut modifier indépendamment le contenu.

Copie d'un tableau

Faisons à présent la même chose avec deux tableaux :



				

→ bizarre ! La modification du tableau b semble s'être "répercutée" dans le tableau a !!

Une explication

Les deux tableaux ne semblent donc pas être indépendants, et en réalité, c'est exactement ce qu'il se passe...pour mieux comprendre, il faut se plonger un peu dans la manière dont Python gère la mémoire : cette page explique notamment très bien en quoi le modèle de la "boîte dans laquelle on met une donnée" pour les variables est en fait complètement faux en Python ( elle est vraie dans d'autres langages plus bas niveau, comme le C par exemple ).

En résumé, une variable en Python représente en réalité une "étiquette" VERS un emplacement mémoire stockant une donnée; une opération de "copie" entre deux variables avec l'opérateur d'affectation = ne crée pas un nouvel emplacement mémoire, mais une nouvelle étiquette, un "alias" de la première, les deux pointant donc exactement vers le même emplacement mémoire.
L'avantage étant qu'on encombre moins la mémoire centrale puisque les données stockées ne figurent qu'en un seul emplacement mémoire.

Dans le cas des types de variable mutables comme les tableaux, modifier une des variables reviendra donc à modifier également l'autre.
Par contre, ce problème n'apparaît pas avec les types immutables comme les entiers ou les chaînes de caractères, pour lesquels Python crée bien un nouvel emplacement mémoire dès qu'il "détecte" que l'on veut modifier une copie d'une variable.

Un type est immutable si la valeur d'une variable de ce type ne peut changer que par l'affectation d'une nouvelle valeur à cette variable. Dans le cas contraire, il sera mutable.

Une copie d'un objet mutable ( comme un tableau ) à l’aide de l’opérateur d’affectation = constitue généralement une erreur.

Copie "légère" et copie "profonde"

Copie "légère"

Il faut en réalité copier élément par élément un tableau dans une nouvelle variable pour en réaliser une "vraie" copie :



				

On peut aussi bien sûr ( on doit ! ) faire une copie par compréhension :


b = [element for element in a]
b = [a[i] for i in range(len(a))]
b = a[:]  # encore plus simple !
			

Python fait une copie dite "légère" des éléments du tableau, mais c'est toujours l'adresse mémoire des objets qui est copiée. Aussi, si l'un des éléments du tableau est aussi un tableau ( nous verrons cela au chapitre suivant ), on retombe sur le même problème.

Copie "profonde"

Pour contourner totalement le problème, il faut utiliser la méthode tableau.deepcopy() du module copy qui effectue une copie dite "profonde":


from copy import deepcopy
.....

b = deepcopy(a) # copie "profonde"
			

Tableau en paramètre de fonction

Une fonction peut sans problème prendre comme paramètre un tableau ; à l'appel de la fonction, il faudra donc lui passer un tableau comme argument.

Mais là aussi, il y a quelques aspects auxquels il faut bien faire attention.

Paramètre immutable

Voila une fonction qui prend comme paramètre un flottant représentant une température en degré Celsius, et renvoie sa valeur en degré Fahrenheit :



				

→ le nom de variable temperature est utilisé à deux endroits différents :

Mais à l'exécution, on se rend bien compte qu'il s'agit en réalité de deux variables différentes : la modification de temperature dans la fonction ne modifie pas temperature du programme principal.

En effet, une variable initialisée dans une fonction, ou qui en constitue un paramètre, à une portée locale à cette fonction : elle n'est créée et "n'existe" que pendant que la fonction s’exécute, et est "oubliée" à la sortie de celle-ci; deux variables immutables ayant le même nom peuvent donc coexister en tant que variable locale à une fonction et globale dans le programme principal sans risque "d'interférence" entre les deux, ce sont en réalité deux objets bien distincts.

Et qu'en est-il avec les tableaux ?

Paramètre mutable



				

→ la variable temperature a été modifiée dans la fonction, et cette modification "persiste" après la fin de la fonction;

Ben, facile de résoudre le problème : il suffit de donner un autre nom au paramètre :



				

→ et non, toujours le même problème !!

Conclusion

Quelque soit le nom utilisé pour le désigner, il n'y a donc qu'un unique objet correspondant au tableau dans tout le script, et ce, toujours à cause du caractère mutable d'un tableau ( un objet mutable est toujours de portée globale dans un script.)

Le problème dans le cas du script ci-dessus : le tableau a été complètement modifié par la fonction, et on a donc perdu les données initiales. On parle d'effet de bord de la fonction, au sens "d'effet secondaire" ou "effet indésirable".
Cela ne se serait bien entendu pas produit si l'on avait au préalable pris soin, au début de la fonction, de faire une copie du tableau passé en paramètre, et en ne modifiant que cette copie :



				

→ on résout ainsi le problème en créant une nouvelle variable, locale à la fonction, et donc bien distincte de celle du programme principal.

D'où cette deuxième autre règle primordiale :

Si dans une fonction on veut modifier les données d'un objet mutable ( comme un tableau ) mais en conserver les données d'origine, on prendra soin d'en faire au préalable une copie et de ne modifier que cette copie.

Attention également au fait que l'on peut donc modifier un tableau dans une fonction, même si on ne l'a pas passé comme argument à cette fonction !!

A titre de documentation, une page pour aller plus loin avec ces histoires de mutable/immutable...

Applications autour des tableaux

La vie scolaire (exercice_C05.py)

Dans un lycée, il y a 10 classes de seconde. L’effectif par classe est limité à 25 pour que les professeurs puissent assurer un suivi personnalisé, et les élèves travailler dans des conditions satisfaisantes (on peut rêver ).

Travail à faire

  1. Écrire l'instruction qui construit par compréhension un tableau de 10 éléments, chaque élément correspondant à l'effectif d'une classe, de valeur choisie aléatoirement entre 20 et 30.
  2. Écrire une fonction compteSurplus(L) qui prend en argument le tableau des effectifs, et qui renvoie, sous forme d'un autre tableau, le nombre d’élèves à enlever si l’effectif dépasse 25.
    Ex. : compteSurplus([24, 25, 26]) renvoie [0, 0, 1]
  3. Écrire une fonction repartitionPossible(L) qui renvoie :
    • un tableau des modifications à effectuer dans le cas où c’est possible.
      Ex. ​ : repartitionPossible([24, 25, 26]) renvoie [1, 0, − 1] )
    • si aucune répartition n'est possible, il faut créer une nouvelle classe et y placer tous les élèves en surplus.
      Ex. : repartitionPossible([24, 25, 27]) renvoie [24, 25, 25, 2].
  4. Réfléchir à une modification de la fonction repartitionPossible(L) pour que l’éventuelle classe créée ne soit pas « trop déséquilibrée » par rapport aux autres.
    Ex. : repartitionPossible([24, 25, 27]) pourrait donner [19, 19, 19, 19].

def compteSurplus(L:list)-> list:
	............
	return tab

def repartitionPossible(L:list)->list:
	...........
	return L_modif
			

Indications

on rappelle l'existence de la fonction randint() qui permet de tirer au hasard un nombre entre deux bornes :


	from random import randint  # en début de script

	alea = randint(1,100)  # x contient un nombre aléatoire entre 1 et 100
					

Lien vers les RÉPONSES

Thérapie génique sur l'ADN (exercice_C06.py)

L'ADN est le support de l'hérédité. C'est une très longue molécule sur laquelle l'information génétique est codée par un ensemble de 4 "lettres" appelées bases azotées, notées A, T, G et C.

Un gène est un ensemble d'un certain nombre de bases qui se suivent sur la molécule d'ADN.

Vous avez peut-être entendu parler de la technique d'édition du génome CRISPR-Cas9, dont la découverte a valu le prix Nobel 2020 de chimie à la chercheuse française Emmanuelle Charpentier. Cette technique appelée "ciseaux moléculaires" permet de cibler un gène particulier dans une séquence d'ADN et de le modifier précisément, ouvrant la voie à de potentielles avancées dans la thérapie génique de certaines maladies.

Le but est d'écrire un script faisant l'analyse d'un ADN de 10000 bases azotées généré aléatoirement par Python afin de "détecter" un gène particulier, puis de "l'éditer" pour le modifier.

Molécule d'ADN

Travail à faire

  1. Écrire une fonction genereADN(n) qui renvoie un tableau de n bases azotées représentant une séquence d'ADN ( "tableau des gènes").
    Pour cela :
    • définir un tuple nommé bases de 4 éléments contenant les caractères 'A', 'T', 'G' et 'C'
    • créer et renvoyer un tableau de n éléments, dont chaque élément est un des caractères du tuple bases choisi au hasard.
  2. Écrire une fonction chercherGene(L, gene) qui prend en argument le tableau des gènes et une chaîne de caractères représentant un gène recherché, et qui renvoie :
    • le ( ou les ) index dans le tableau au(x)quel(s) on trouve le gène recherché ( sous forme d'un tableau )
    • False si le gène recherché ne se trouve pas dans le tableau des gènes
  3. écrire une fonction crisprCas9(L, i, nvGene) qui prend en argument le tableau des gènes, l'index du gène à éditer et une chaîne de caractères représentant le nouveau gène à "coder" à l'index i, et qui renvoie le tableau modifié des gènes.
  4. Tester vos fonctions sur une courte séquence d'ADN pour vérifier leur bon fonctionnement.
  5. Générer maintenant une séquence de 10000 bases azotées ( ne surtout pas l'afficher ! ).
    Le gène 'TAGAGA' est le gène responsable du comportement de rébellion des individus face à l'autorité; remplacer ce gène par le gène 'GATACA' qui induit au contraire un caractère de soumission.

def genereADN(n:int)-> list:
	............
	return adn

def chercherGene(L:list)->list:
	...........
	return tab

def crisprCas9(L:list, i:int, nvGene:str)->list:
	..........
	return L_modif
			

Indications

  • il faut donc parcourir tout le tableau des gènes afin d'y détecter le gène recherché.
  • Le gène à rechercher est une chaîne de caractères; pour la transformer en un tableau, il faut recopier ses caractères dans un tableau ( on rappelle qu'une chaîne est aussi un objet itérable...). Penser à utiliser une construction par compréhension.
  • la fonction rechercheGene() est clairement la plus compliquée...
    Voici une proposition d'algorithme de cette recherche : on parcourt successivement toutes les "bases azotées" de la séquence d'ADN, et à chaque "base", on teste si le gène est présent ou non à partir de cette position :

    Recherche d'un gène
    
    	fonction rechercherGene(adn:tableau des genes, gene:chaîne de caractères):
    		gene = tableau du gène recherché
    		trouve = booléen
    		resultat = tableau des index au(x)quel(s) on a trouvé le gène dans le tableau adn
    
    		pour chaque élément i de adn :
    			trouve = VRAI
    			pour chaque élément j de gene :
    				si adn[i+j] différent de gene[j] :
    					trouve = FAUX
    				fin si
    			fin pour
    			si trouve = VRAI:
    				ajouter au tableau resultat la position du gène dans adn
    			fin si
    		fin pour
    
    		si le tableau resultat n'est pas vide:
    			renvoyer resultat
    		sinon:
    			renvoyer FAUX
    		fin si
    				

    La variable booléenne trouve est un drapeau, c'est à dire une donnée qui garde la trace et signale si les caractères correspondent ou pas. Ce drapeau, initialement à VRAI, est modifié à FAUX dès que l'algorithme rencontre un caractère qui ne correspond pas dans les deux tableaux.

    La condition à écrire à la ligne 10 pour "basculer" le drapeau à FAUX n'est pas évidente, il faut bien bien y réfléchir !

    On utilise donc deux boucles imbriquées pour réaliser, d'une part, le parcours du tableau des gènes, et , d'autre part, à chaque position de ce tableau le parcours du tableau du gène recherché. En tout, l'algorithme fait donc len(tableau des gènes) x len(gène) "tours", ce qui peut prendre pas mal de temps si le tableau des gènes est grand !

    Cette approche est donc dite "de force brute", au sens de "pas très subtile mais efficace" : il existe des manières plus optimisées de rechercher une séquence de caractères dans une phrase ou un tableau, vous en verrez en Terminale...

Lien vers les RÉPONSES

Mini-projet : le jeu de la Bataille (exercice_C07_bataille.py)

Vous avez bien entendu déjà joué à la Bataille fermée : on partage les cartes d'un jeu entre les joueurs ( deux le plus souvent ); à chaque tour chaque joueur pose à l'endroit la carte du dessus de son tas. Si sa carte a la plus grande valeur, il ramasse toutes les cartes posées et les place en dessous de son tas.
En cas d'égalité, il y a "bataille" : on place une carte à l'envers sur la précédente, puis on en pose une autre à l'endroit; on recommence tant qu'il n'y a pas de gagnant pour le tour.

Les cartes par valeur croissante sont : 7, 8, 9, 10, Valet, Dame, Roi, As ( symbolisés par la suite par : 7, 8, 9, D, J, Q, K , A )

Le gagnant est celui qui a ramassé toutes les cartes du jeu.

Jeu de cartes

En vous limitant à un jeu de 32 cartes, vous allez coder un jeu de Bataille fermée : vous jouerez contre l'ordinateur ou contre un de vos camarades.
A chaque tour de jeu devront être indiquées les cartes posées, mais pas les cartes restantes dans le tas.


	JEU DE LA BATAILLE

	Tour 1 :
	L'ordinateur pose : D Carreau
	Vous posez : A Coeur

	Vous gagnez le tour !
	Vous avez 17 cartes. L'ordinateur a 15 cartes. Pas de gagnant pour la partie.

	Tour 2 :
	L'ordinateur pose : A Trèfle
	Vous posez : 7 Pique

	L'ordinateur gagne le tour !
	Vous avez 16 cartes. L'ordinateur a 16 cartes. Pas de gagnant pour la partie.

	Tour 3 :
	L'ordinateur pose : K Pique
	Vous posez : K Coeur

	BATAILLE !

	................
					

Travail à faire

  1. écrire une fonction genereJeux() qui renvoie deux tableaux représentant les tas de cartes initiaux de chacun des deux joueurs.
    • Vous utiliserez le tableau cartes ci-dessous qui contient déjà les 32 cartes d'un jeu complet, et la fonction shuffle() du module random qui permet de mélanger aléatoirement les éléments d'un tableau :
      
      from random import shuffle # en début de script
      
      cartes = ['7 Carreau','8 Carreau','9 Carreau','D Carreau','J Carreau','Q Carreau','K Carreau','A Carreau','7 Coeur','8 Coeur','9 Coeur','D Coeur','J Coeur','Q Coeur','K Coeur','A Coeur','7 Trèfle','8 Trèfle','9 Trèfle','D Trèfle','J Trèfle','Q Trèfle','K Trèfle','A Trèfle','7 Pique','8 Pique','9 Pique','D Pique','J Pique','Q Pique','K Pique','A Pique']
      
      shuffle(cartes)  # mélange aléatoirement les éléments du tableau cartes
      							

      Dans ce tableau, chaque carte est une chaîne de caractères, avec un premier caractère indiquant le rang de la carte, puis, après un espace, sa couleur.

    • les deux tableaux joueurs seront de taille fixe, égale à 32 éléments ( le maximum de cartes qu'un joueur peut avoir dans son jeu !); à la création, ces deux tableaux pourront contenir des 0 ou des éléments None, puis, dans un deuxième temps, ils seront "remplis" à partir des éléments du tableau cartes de façon à ce que chaque joueur reçoive la moitié des cartes.
  2. écrire une fonction test_cartes(carte1, carte2) qui compare deux cartes, et qui renvoie quelle est la plus grande ou si il y a une bataille.
    Pour évaluer quelle carte a la plus grande valeur, il faut comparer le premier caractère du début du nom des deux cartes.
    Mais ensuite, il ne suffit pas de simplement comparer ces lettres pour savoir quelle est la plus grande valeur !
    Le plus simple est alors de se servir d'un tuple contenant les valeurs des cartes par ordre croissant :
    
    valeurs = ('7', '8', '9', 'D', 'J', 'Q', 'K', 'A')
    					
    La carte de plus forte valeur est alors celle qui a l'index le plus grand dans ce tuple.
  3. écrire une fonction jouer(joueur1, joueur2) qui code le déroulement de la partie, en affichant à chaque tour la carte jouée par chaque joueur.
    Cette fonction utilisera le résultat renvoyé par la fonction test_cartes() écrite précédemment.
    • réfléchir à la manière de gérer les tas de cartes : la carte jouée est celle du dessus du tas; il faut donc pouvoir repérer le "haut" de chaque tas pour savoir quelle carte sera jouée ( le début du tableau ? la fin ? )
    • à chaque tour, il faut déterminer sous quel tas il faut venir ajouter les deux cartes gagnées : comment réaliser cette "insertion" des cartes dans le "bon" tableau ?
    • de même, il faut aussi retirer les deux cartes jouées du haut de chaque tas : comment réaliser cette "suppression" d'élément ?
    • à la fin de chaque tour de jeu, il faudra bien entendu déterminer si un des joueurs a gagné : la fonction devra alors renvoyer le nom du joueur ayant gagné.
  4. le plus délicat sera de coder les batailles; garder cette étape pour la fin de votre codage. Vous pouvez même écrire une fonction à part dédiée à la gestion des batailles.

def genereJeux()->list:
    .......

    return jeu1, jeu2


def test_carte(c1:str, c2:str)->str:
    .......

    return "carte_la_plus_grande_ou_bataille"


def jeu(j1:list, j2:list)->str:
	.......

	return "Gagnant"
			

Lien vers les RÉPONSES

Un visuel plus agréable...

Votre jeu fonctionne ? Très bien ! Maintenant, si vous voulez un affichage un peu plus agréable des cartes, vous pouvez utiliser cette archive.
Elle permet un affichage graphique des cartes posées par le joueur et le PC dans une fenêtre.

Attention, la fenêtre où s'affiche les cartes risque de "passer derrière" celle de Pyzo : réduisez la fenêtre de ce dernier de façon à avoir les deux en même temps sur le bureau.