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...
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.
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 !!
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.
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.
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"
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.
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 ?
→ 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 !!
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...
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 ).
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.compteSurplus([24, 25, 26])
renvoie [0, 0, 1]
repartitionPossible(L)
qui renvoie :
repartitionPossible([24, 25, 26])
renvoie [1, 0, − 1]
)repartitionPossible([24, 25, 27])
renvoie [24, 25, 25, 2]
.repartitionPossible(L)
pour que l’éventuelle classe créée ne soit pas « trop déséquilibrée » par rapport aux autres.
def compteSurplus(L:list)-> list:
............
return tab
def repartitionPossible(L:list)->list:
...........
return L_modif
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
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.
genereADN(n)
qui renvoie un tableau de n bases azotées représentant une séquence d'ADN ( "tableau des gènes").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 :False
si le gène recherché ne se trouve pas dans le tableau des gènescrisprCas9(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.
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
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 :
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...
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.
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 !
................
genereJeux()
qui renvoie deux tableaux représentant les tas de cartes initiaux de chacun des deux joueurs.
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.
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.test_cartes(carte1, carte2)
qui compare deux cartes, et qui renvoie quelle est la plus grande ou si il y a une bataille.
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.
jouer(joueur1, joueur2)
qui code le déroulement de la partie, en affichant à chaque tour la carte jouée par chaque joueur.test_cartes()
écrite précédemment.
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"
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.
Bataille
y sera créé.bataille_gui.py
; il est à compléter avec votre propre script, à l'endroit qui y est indiqué :
##########################
# VOTRE SCRIPT #
# ICI #
##########################
affiche_cartes(carte_joueur1, carte_joueur2)
fen.update() # à ne pas oublier...
carte_joueur et carte_joueur2 sont deux chaînes correspondant aux deux cartes à afficher; bien les indiquer dans cet ordre, et respecter la syntaxe de le tableau cartes.
Vous disposez en plus d'une carte nommée dos_carte
pour afficher un dos de carte lors des batailles.
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.