Sous-chaînes, transformations¶
Modifier une chaîne¶
Les caractères d’une chaîne ne sont pas modifiables :
1 2 3 | s = "taTou"
s[2] = "B"
print(s)
|
4 | TypeError: 'str' object does not support item assignment
|
- Ligne 2 : on tente de changer le caractère
T
de la chaîne"taTou"
en"B"
.
Par construction, les chaînes de caractères sont de type immuable : leur contenu ne peut être modifié. Non seulement, on ne peut pas changer un caractère en un autre, mais on ne peut en supprimer ni en rajouter, à quelque endroit que ce soit.
Affectation augmentée par addition¶
Considérons la réaffection de variable suivante :
1 2 3 4 5 | a = "Rose"
b = "Orange"
a = a + b
print(a)
|
6 | RoseOrange
|
La variable a
a été réaffectée à elle-même après modification. La ligne 3 peut être raccourcie en Python avec une instruction d’affectation augmentée, notée +=
, ce qui donne le nouveau code suivant :
1 2 3 4 5 | a = "Rose"
b = "Orange"
a += b
print(a)
|
6 | RoseOrange
|
La documentation de référence du langage Python n’explique pas précisément comment la somme a + b
(ligne 3 du premier code) est réalisée : des copies de a
et de b
sont-elles réalisées avant que la somme ne soit réalisée ? Cette question est importante car elle va expliquer comment une somme telle a + b + c
va être réalisée.
Toujours-est-il que l’affectation augmentée n’est pas qu’un raccourci syntaxique, l’opération est « en place » : elle éviterait une copie de a
et serait plus efficace.
Des exemples seront montrés ultérieurement.
Créer une chaîne à partir de la chaîne vide¶
Soit à construire une chaîne z
dont les caractères sont exactement les consonnes d’une chaîne s
donnée. Par exemple, si s
est la chaîne broccoli
alors la chaîne cherchée est brccl
.
Pour cela, il suffit de parcourir la chaîne s
et d’ajouter à une chaîne initialement vide (disons z
) le caractère courant de s
si le caractère est une consonne (et donc s’il n’est pas une voyelle). D’où le code
1 2 3 4 5 6 7 8 9 10 | s= "broccoli"
VOYELLES ="aeiouy"
z = ""
for c in s:
if c not in VOYELLES:
z += c
print(s)
print(z)
|
11 12 | broccoli
brccl
|
- Lignes 3, 10 et 12 : la chaîne à construire est initialement vide.
- Lignes 5-7 : la chaîne
s
est agrandi au fur et à mesure par ajout d’une consonne.
C’est le même type de construction que pour une liste initialement vide à laquelle on applique la méthode append
.
Extraire les caractères individuels d’une chaîne¶
On se donne une chaîne de caractères s
, par exemple s = "abricot"
et on cherche à en extraire les caractères successifs pour les placer dans une liste. Pour cela, il suffit d’appliquer le constructeur list
à s
:
s = "abricot"
L = list(s)
print(L)
['a', 'b', 'r', 'i', 'c', 'o', 't']
La méthode join
¶
On dispose d’une liste de chaînes, par exemple les 4 chaînes suivantes :
File Open Options Display
et on veut rassembler ces chaînes en une seule mais en les séparant avec un séparateur donné, par exemple avec le séparateur
>
(le symbole >
entouré d’espaces), ce qui donnerait :
File > Open > Options > Display
C’est ce que permet de faire la méthode join
:
1 2 3 | L = ["File", "Open", "Options", "Display"]
s = " > ".join(L)
print(s)
|
4 | File > Open > Options > Display
|
- Ligne 1 : les chaînes à rassembler sont placées dans une liste.
- Ligne 2 : noter que la méthode s’utilise comme attribut du séparateur et qu’elle prend en argument la liste.
- Ligne 4 : on observe bien le rassemblement des chaînes de la liste
L
.
L’usage de la méthode join
n’est pas toujours considéré comme intuitif (voir ici par exemple), mais join
est une méthode du séparateur (d’une chaîne donc), et pas d’une liste.
Séparateurs courants pour la méthode join
¶
Quand on applique sep.join(L)
, tout séparateur sep
peut être envisagé mais en voici trois très courants :
- la chaîne vide
""
- le saut de ligne
"\n"
- l’espace
" "
Par exemple
1 2 3 4 5 6 7 8 9 10 11 12 | L = ["Mars", "Jupiter", "Uranus", "Neptune"]
s = "".join(L)
print(s)
print()
s = "\n".join(L)
print(s)
print()
s = " ".join(L)
print(s)
|
qui affiche
1 2 3 4 5 6 7 8 | MarsJupiterUranusNeptune
Mars
Jupiter
Uranus
Neptune
Mars Jupiter Uranus Neptune
|
- Ligne 1 : C’est une technique idiomatique (ligne 3 du code) de concaténation de chaînes où on utilise
''.join(L)
avec un séparateur vide. - Lignes 3-6 : les chaînes sont placées les unes en-dessous des autres avec le séparateur saut de ligne (le caractère
\n
). - Ligne 9 : les chaînes sont placées côte-à-côte, séparées par exactement un espace et uniquement entre les chaînes, pas à la fin ni au début.
Séparer une chaîne suivant des sous-chaînes avec la méthode split
¶
On dispose d’un numéro de téléphone, par exemple, 08-42-20-32-17
et on cherche à extraire les composantes de ce numéro de téléphone, à savoir 08, 42, etc. Autrement dit, il s’agit de séparer le numéro suivant le caractère -
. C’est typiquement ce que peut faire la méthode split
:
numero = "08-42-20-32-17"
L = numero.split("-")
print(L)
print(numero)
['08', '42', '20', '32', '17']
08-42-20-32-17
split
est une méthode de chaîne. Un appel numero.split(sep)
a pour fonction de séparer la chaîne numero
suivant le séparateur sep
, lui aussi une chaîne. Dans l’exemple ci-dessus, sep
est la chaîne "-"
. L’appel à la méthode split
renvoie la liste des chaînes qui sont séparées par le séparateur, chacune des chaînes étant une sous-chaîne de la chaîne initiale. La chaîne initiale n’est pas modifiée, cf. la dernière ligne de la sortie ci-dessus (de toute façon, une chaîne n’est pas modifiable).
Noter que s.split(sep)
renvoie une liste de chaînes qui ne contiennent plus sep
comme sous chaîne.
Le séparateur peut être une chaîne arbitraire (mais non vide), éventuellement contenant plusieurs caractères, comme ci-dessous :
s = "marabout -> bout de ficelle -> selle de cheval -> cheval de course"
L = s.split(" -> ")
print(L)
['marabout', 'bout de ficelle', 'selle de cheval', 'cheval de course']
Situation ambiguë¶
Il se peut que le séparateur empiète sur lui-même, par exemple si s = vvvABABAvvv
et le séparateur est ABA
alors l’occurrence du séparateur qui commence en 4e position n’est pas disjointe de l’occurrence qui commence en 6e position. Voici comment split
réalise le découpage :
s="vvvABABAvvv"
print(s.split("ABA"))
['vvv', 'BAvvv']
On voit donc que le découpage se fait de la gauche vers la droite, chaque sous-chaîne valant le séparateur est écartée et la recherche du séparateur continue à l’indice qui suit la fin du séparateur.
Règle¶
L’appel ma_chaine.split(mon_sep)
élimine sep
de s
, en progressant dans s
de la gauche vers la droite. Les tronçons de s
restants forment la liste L
renvoyée par split
et cette liste L
de chaînes est telle que si on intercale entre deux éléments consécutif de L
la chaîne mon_sep
, on obtient une chaîne identique à ma_chaine
. Si la chaîne ma_chaine
contient n occurrences successives et disjointes de mon_sep
alors la liste L
contiendra n+1 éléments. Le séparateur doit être une chaîne non vide.
L’option maxsplit
de la méthode split
¶
La méthode split
dispose d’une option maxsplit
qui limite le nombre de retraits du séparateur. Voici un exemple :
1 2 3 | numero = "08-42-20-32-17"
L = numero.split("-", maxsplit=2)
print(L)
|
4 | ['08', '42', '20-32-17']
|
Le procédé est le même que pour split
sans cette option : la chaîne est parcourue de la gauche vers la droite et sont rétirées de la chaîne au plus les maxplit
premières occurrences du séparateur.
Méthode split
: blancs et saut de ligne¶
Si on ne donne aucun argument à la méthode split
, la séparation est effectuée suivant les blancs :
s = "L'eau\nLa terre\nLe feu"
L = s.split()
print(L)
["L'eau", 'La', 'terre', 'Le', 'feu']
- La chaîne
s
est découpée suivant les blancs (espace et saut de ligne).
On notera que tout groupement contigu de blancs fait alors office de séparateur unique :
s = "L' eau \n La \n terre \t \n\nLe feu"
print(s)
L = s.split()
print(L)
L' eau
La
terre
Le feu
["L'", 'eau', 'La', 'terre', 'Le', 'feu']
Ci-dessus, les blancs sont constitués d’espaces, de sauts de ligne et de tabulations horizontales.
Un usage courant de split
est de découper un texte en une suite de lignes avec le séparateur "\n"
:
s = "L'eau\nLa terre\nLe feu"
L = s.split('\n')
print(L)
["L'eau", 'La terre', 'Le feu']
Méthode splitlines
: découper en lignes successives¶
On dispose d’un texte, avec des sauts de lignes, par exemple
--------
L'eau
La terre
Le feu
Le ciel
--------
(noter qu’il n’y a pas de saut de ligne final après la 2e ligne de tirets) et on souhaite le découper en une liste L
de chaînes, chaque élément de L
étant formé d’une ligne (contenant au plus un saut de ligne). La méthode split
ne convient pas car elle perd les sauts de lignes et, même si on rajoute le saut de ligne, cela ne convient pas toujours. C’est pour cela qu’existe la méthode splitlines
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | s="""--------
L'eau
La terre
Le feu
Le ciel
--------"""
L=s.splitlines(True)
print(L)
L=s.splitlines()
print(L)
M=s.split('\n')
print(L == M)
|
19 20 21 22 23 | ['--------\n', "L'eau\n", '\n', 'La terre\n', '\n',
'Le feu\n', '\n', 'Le ciel\n', '\n', '--------']
['--------', "L'eau", '', 'La terre', '',
'Le feu', '', 'Le ciel', '', '--------']
True
|
- Lignes 1-10 : pour alléger la saisie, j’ai utilisé une chaîne littérale triple, voir Les différents délimiteurs de chaînes littérales.
- Ligne 11 : la méthode
splitlines
possède un argument nommékeepends
, placé ici àTrue
ce qui entraîne que la ligne complète et son saut de ligne sont capturés, y compris des espaces avant le saut de ligne. - Ligne 14 : si on ignore l’argument
keepends
, la ligne complète privée de son saut de ligne est capturée, y compris des espaces avant le saut de ligne. - Lignes 19-23 : Noter la présence ou l’absence de saut de ligne selon les cas (j’ai reformaté les deux lignes pour une meilleure lisibilité).
join
est l’inverse de split
¶
La méthode join
recompose de ce que la méthode split
décompose :
z="roseXorangeXbegonia"
L=z.split('X')
s='X'.join(L)
print(s==z)
True
autrement dit, on a toujours sep.join(s.split(sep)) == s
.
En revanche, la méthode split
n’est pas exactement l’inverse de la méthode join
, autrement dit, il n’est pas toujours vrai que sep.join(L).split(sep)
et L
soient identiques. Par exemple
L=['rose', 'oraXge', 'begonia']
sep = 'X'
z=sep.join(L)
M=z.split(sep)
print(M)
['rose', 'ora', 'ge', 'begonia']
Comme on le constate, la liste M
n’est pas identique à la liste originale L
; cela tient au fait que l’une des chaînes de L
contient le séparateur comme sous-chaîne.
En revanche, si L
est une liste de chaînes telle qu’aucune des chaînes ne contient une chaîne sep
donnée comme sous-chaîne alors les chaînes sep.join(L).split(sep)
et L
sont identiques.
La méthode join
vs l’addition augmentée¶
Voci un code qui sélectionne les consonnes présentes dans une chaîne donnée :
s= "broccoli"
VOYELLES ="aeiouy"
z = ""
for c in s:
if c not in VOYELLES:
z += c
print(z)
brccl
Toutefois, le procédé est assez laborieux. Dans ce genre de situations où on effectue des concaténations successives, on peut se demander si on ne peut pas appliquer la méthode join
. Ce genre de code peut nécessiter davantage de connaissances en Python. Ici, l’opération est tout à fait réalisable : il suffit de collecter les consonnes et de les assembler avec join
appliqué à la chaîne vide :
1 2 3 4 5 6 | s= "broccoli"
VOYELLES ="aeiouy"
z = ''.join(c for c in s if c not in VOYELLES)
print(z)
|
7 | brccl
|
Ici, ligne 4, on a utilisé une expression génératrice pour collecter les consonnes. Elles sont ensuite soudées avec join
.
La méthode join
appliquée à une liste courte¶
Si la liste est vide, join
renverra une chaîne vide :
L = []
s = " > ".join(L)
print(s)
Si la liste contient un seul élément, la chaîne renvoyée ignore le séparateur :
L = ["File"]
s = " > ".join(L)
print(s)
File
Méthode split
: chaînes vides ou pleine après séparation¶
Chaînes vides¶
Il se peut que des chaînes de la liste renvoyée par split
soient vides :
s = "ABAxyABAABAxxABA"
sep= "ABA"
L=s.split(sep)
print(L)
['', 'xy', '', 'xx', '']
Rappelons en effet que si la sous-chaîne sep
est présente en \mathtt{k} séquences disjointes successives dans la chaîne \mathtt{s} alors s.split(sep)
est une liste de \mathtt{k+1} sous-chaînes délimitées par le retrait des \mathtt{k} occurrences disjointes de \mathtt{sep} dans s
.
Dans le cas de l’exemple, sep = "ABA"
apparaît, de gauche à droite, suivant 4 sous-chaînes disjointes. Plaçons un délimiteur sous forme de trait vertical à chaque extrémité de la chaîne :
|ABAxyABAABAxxABA|
Dans la chaîne précédente, plaçons dans un bloc chaque sous-chaîne ABA
en l’encadrant par un délimiteur :
||ABA|xy|ABA||ABA|xx|ABA||
Il suffit alors de lire, de la gauche vers la droite, la succession des contenus entre deux séparateurs qui n’encadrent pas la sous-chaîne aba
. On obtient, en écrivant verticalement pour une meilleure lisibilité :
||
|xy|
||
|xx|
||
ce qui explique pourquoi la valeur de s.split(sep)
est :
['', 'xy', '', 'xx', '']
Séparateur absent¶
Lors d’un appel s.split(sep)
, si le séparateur sep
n’est pas présent dans la chaîne, la liste renvoyée par split
est une liste dont le seul élément est la chaîne elle-même :
s = "bonjour"
print(s.split("X"))
['bonjour']
C’est cohérent avec le fait que s.split(sep)
renvoie une liste de longueur 1 de plus que le nombre d’occurrences disjointes de sep
dans s
qui ici vaut 0.
Séparer des caractères, des lignes avec join
¶
La méthode join
s’applique non seulement à une liste (ou a un tuple) mais aussi à une simple chaîne :
s="_".join("ORANGE")
print(s)
O_R_A_N_G_E
En réalité, la méthode join
peut prendre en argument n’importe quel itérable de chaînes. Par exemple, supposons qu’on dispose d’un fichier fruits.txt
, de contenu
pomme
poire
fraise
abricot
et qu’on exécute le code suivant :
1 2 3 | fichier = open("fruits.txt")
t = "OU BIEN ".join(fichier)
print(t)
|
À la ligne 1, on ouvre le fichier fruits.txt
pour obtenir un objet Python de type file
. Alors, comme fichier
est un itérable sur les lignes du fichiers (saut de ligne inclus), la ligne 2 aura pour effet de placer dans une liste t
les lignes du fichier séparées par la chaîne OU BIEN, comme on peut le voir ci-dessous :
pomme
OU BIEN poire
OU BIEN fraise
OU BIEN abricot
Somme de chaînes coûteuse¶
Soit n
un entier positif et soit c
une chaîne donnée. On cherche à construire une nouvelle chaîne où c
est répété n
fois et séparé du suivant par un tiret, par exemple si c = « AAAAA » et n = 3
alors la chaîne à construire est :
AAAAA-AAAAA-AAAAA
Utilisons une boucle for
pour créer la chaîne (ce n’est pas la bonne méthode pour faire cela en Python mais la question n’est pas là) :
c = "A" * 5
N = 3
s = c
for i in range(N - 1):
s = s + '-' + c
print(len(s))
AAAAA-AAAAA-AAAAA
Prenons maintenant une assez valeur grande pour n
, par exemple n=100_000
. Le temps d’exécution va alors être prohibitif :
1 2 3 4 5 6 7 8 9 | %%timeit -r2
c = "A" * 5
N = 100_000
s = c
for i in range(N - 1):
s = s + '-' + c
print(len(s))
|
10 11 12 13 | 599999
599999
599999
1 loop, best of 2: 3.41 s per loop
|
On lit le temps d’exécution en dernière ligne : plus de 3 secondes alors que ça devrait être quelques dizaines de ms. Le code a été écrit dans une cellule Jupyter Notebook ce qui permet de mesurer le temps d’exécution moyen (cf. ligne 1, ici 2 répétitions de l’exécution)
Pourquoi ce code est-il si lent ? Réponse : à chaque étape de la boucle for
(ligne 7), l’expression s + '-' + c
est évaluée. Pour cela, l’interpréteur Python regroupe l’expression de la manière suivante : (s + '-') + c
. Pour calculer cette somme, il doit d’abord calculer (s + '-')
ce qui va l’obliger à recopier s
et à placer au bout le tiret. Comme la longueur de s
est de plus en plus longue, le calcul va être de plus en plus coûteux. La complexité est quadratique en n
(si on multiple par 10 le nombre de répétitions, le temps de calcul est multiplié par 100).
Cherchons une alternative, toujours en utilisant une boucle for
. L’opération à répéter est une réaffection de s
et qu’on peut aussi écrire :
s = s + ('-' + c)
On peut donc la remplacer par une affectation augmentée +=
qui est beaucoup plus efficace car elle réalise la concaténation en place et la complexité sera quasi linéaire. Et en effet, le code met moins de 20 ms cette fois :
%%timeit -n1 -r1
c = "A" * 5
N = 100_000
s = c
for i in range(N - 1):
s += '-' + c
print(len(s))
599999
1 loop, best of 1: 18 ms per loop
Noter que l’astuce précédente n’est pas toujours applicable. Par exemple, donnons-nous un entier, disons n = 20
et supposons qu’on ait à construire la chaîne de caractères formée des entiers (écrits en chiffres) \mathtt{n}, \mathtt{n-1}, etc jusqu’à 1 ce qui, avec l’exemple, donnerait :
\mathtt{n=2019181716151413121110987654321}
La façon assez assez naturelle d’écrire le code serait :
1 2 3 4 5 6 | N = 20
s=''
for i in range(1, N+1):
s = str(i) + s
print(s)
|
7 | 2019181716151413121110987654321
|
et si n
est grand, on va rencontrer la même lenteur d’exécution qu’avec le problème précédent mais, cette fois, sans pouvoir utiliser l’affectation augmentée +=
.
De toute façon, la bonne façon de réaliser les deux tâches en Python était d’utiliser la méthode join
. Par exemple, pour la première chaîne à construire, le code serait :
%%timeit -n1 -r1
N = 100000
c = "A" * 5
s='-'.join(c for i in range(N))
print(len(s))
599999
1 loop, best of 1: 15.3 ms per loop
et qui est légèrement plus rapide et c’est encore meilleur en utilisant une liste en compréhension :
%%timeit -n1 -r1
N = 100000
c = "A" * 5
s='-'.join([c for i in range(N)])
print(len(s))
599999
1 loop, best of 1: 9.32 ms per loop
Différentes approches pour concaténer une liste de chaînes¶
Dans ce paragraphe, nous allons envisager 5 méthodes pour concaténéer une liste de chaînes.
Autant le dire tout de suite, la seule méthode pythonnique et recommandée pour concaténer un itérable de chaînes est d’utiliser la méthode join
sur une chaîne vide :
L = ["Mars", "Jupiter", "Uranus", "Neptune"]
s = "".join(L)
print(s)
MarsJupiterUranusNeptune
Une autre méthode envisageable est de construire la chaîne concaténée à partir d’une chaîne vide en ajoutant itérativement chaque chaîne de la liste :
L = ["Mars", "Jupiter", "Uranus", "Neptune"]
s = ''
for elt in L:
s += elt
print(s)
MarsJupiterUranusNeptune
On pourrait tenter d’utiliser la fonction intégrée sum
pour additionner les chaînes mais Python l’interdit :
L = ["Mars", "Jupiter", "Uranus", "Neptune"]
s = sum(L,[])
print(s)
s = sum(L,[])
TypeError: can only concatenate list (not "str") to list
Une autre méthode qui peut être envisagée est d’utiliser la méthode format
comme suggéré ICI à laquelle on transmet tous les éléments de la liste par décompression :
L = ["Mars", "Jupiter", "Uranus", "Neptune"]
gabarit = "{}"*len(L)
print(gabarit.format(*L))
MarsJupiterUranusNeptune
Une nouvelle possibilité consisterait à exploiter la faculté de la fonction print
à réaliser une concaténation par le biais de son argument nommé sep
et qu’on placerait sur une chaîne vide :
L = ["Mars", "Jupiter", "Uranus", "Neptune"]
print(*L, sep='')
MarsJupiterUranusNeptune
Le problème est de parvenir à capturer la chaîne affichée. Voici comment on peut faire en utilisant le module standard io
:
1 2 3 4 5 6 7 | from io import StringIO
L = ["Mars", "Jupiter", "Uranus", "Neptune"]
s = StringIO()
print(*L, sep='', file=s, end='')
print(s.getvalue())
|
8 | MarsJupiterUranusNeptune
|
- Lignes 1 et 4 : le module standard
io
(qui veut dire input/output) permet de considérer une chaîne de type StringIO comme une « sortie ». - Ligne 6 : la fonction
print
permet, grâce à un argument nomméfile
, d’envoyer le flux de caractères à afficher dans un objet assimilé à un fichier. Ici, ce fichier sera la chaînes
définie précédemment. Par ailleurs, par défaut,print
termine son affichage en plaçant un saut de ligne mais comme nous voulons afficher des chaînes concaténées, on inhibe cette terminaison en utilisant l’argument nomméend=''
(terminaison vide). - Ligne 6 : l’affichage réalisé ne sera pas visible : par défaut, l’argument nommé
file
deprint
est la sortie standardstdout
(en clair, dans la console de texte) et ici il a été changé ens
. - Ligne 7 : pour récupérer le contenu de la chaîne
s
, il faut appeler la méthodegetvalue
.
On peut comparer les performances de ces différentes approches :
from time import perf_counter
from random import randrange
from io import StringIO
def maketest(N, M):
return ['X'*randrange(1, M+1) for _ in range(N)]
def join(A):
return ''.join(A)
def add(A):
s=''
for a in A:
s=s+a
return s
def iadd(A):
s=''
for a in A:
s+=a
return s
def format(A):
N=len(A)
return ("{}"*N).format(*A)
def strIO(A):
s= StringIO()
print(*A, sep='', file=s, end='')
return s.getvalue()
N=3*10**6
M=150
A=maketest(N, M)
F=[join, add, iadd, format, strIO]
check=[]
for f in F:
begin_perf = perf_counter()
L=f(A)
delta = perf_counter() - begin_perf
msg=f"{f.__name__ :<13}: {delta:.2f}s"
check.append(len(L))
print(msg)
print(check==[check[0]]*len(check))
qui affiche
join : 0.13s
add : 0.30s
iadd : 0.30s
format : 0.27s
strIO : 0.55s
True
La méthode join
est la plus rapide, assez largement et c’est d’ailleurs cette méthode qui est recommandée (dans le 2e paragraphe) pour concaténer des chaînes. Curieusement, les fonctions add
utilisant s = s + a
et iadd
utilisant s += a
ont des performances comparables alors que s = s + a
est censé être plus coûteux puisque s + a
recrée une nouvelle chaîne (il semblerait que Python 3 soit capable d’optimiser cette situation dans certains cas, voir ce message).
Remplacer des caractères d’une chaîne¶
On ne peut pas remplacer un caractère d’une chaîne donnée (puisqu’une chaîne est immuable), on ne peut qu’en donner l’illusion en créant une nouvelle chaîne déduite de la chaîne initiale. La méthode de chaîne replace
permet de « remplacer » des blocs de caractères consécutifs d’une chaîne.
Par exemple :
1 2 3 | z="abcXYefgXYh"
print(z.replace("XY","ZZZ"))
print(z)
|
4 5 | abcZZZefgZZZh
abcXYefgXYh
|
- Ligne 1 : la chaîne originale
- Lignes 2 et 4 :
replace
crée une nouvelle chaîne où toute suiteXY
dez
est remplacée parZZZ
. - Lignes 3 et 5 : La chaîne originale
z
est préservée ie les blocsXY
n’ont pas été modifiés.
L’exemple ci-dessus montre qu’il s’agit moins d’un remplacement dans la chaîne originale que d’une copie altérée de la chaîne originale
On pourrait avoir l’illusion d’un vrai remplacement dans la chaîne initiale avec le code suivant:
1 2 3 4 | z = "abcXYefgXYh"
print(z)
z = z.replace("XY","ZZZ")
print(z)
|
5 6 | abcXYefgXYh
abcZZZefgZZZh
|
- Ligne 4 : on pourrait penser que la chaîne
z
a eu ses caractères remplacés. En réalité,z
a été réaffecté à la nouvelle chaîne, la chaîne initiale elle étant perdue.
Plusieurs remplacements successifs¶
Si on veut faire plusieurs remplacements, il faut appliquer plusieurs fois de suite la méthode replace
. Par exemple soit à changer dans la chaîne xxaybyxyxaabyxyab
le x
en A
et le y
en B
:
z="xxaybyxyxaabyxyab"
zz=z.replace("x","A")
zzz=zz.replace("y","B")
print(z)
print(zzz)
xxaybyxyxaabyxyab
AAaBbBABAaabBABab
ce qui pouvait être raccourci en
z="xxaybyxyxaabyxyab"
Z = z.replace("x","A").replace("y", "B")
print(Z)
AAaBbBABAaabBABab
Bien sûr, ces remplacements ont un coût à l’exécution puisque pour les réaliser :
- un algorithme de recherche doit être employé (Python utilise l’algorithme de Boyer-Moore)
- cet algorithme va parcourir toute la chaîne
- il y aura autant de parcours que d’appels à la méthode
replace
.
Dans notre cas, utiliser la méthode translate était plus approprié.
Option de la méthode replace
¶
La méthode replace
possède une option count
qui permet de remplacer un nombre déterminé d’occurrences. Voici un exemple d’utilisation :
z="xxaybyxyxaabyxyab"
zz=z.replace("x","A", 3)
print(z)
print(zz)
xxaybyxyxaabyxyab
AAaybyAyxaabyxyab
Seules les 3 premières occurrrences du caractère 'x'
dans la chaîne z
(sur un total de 5) ont été remplacées par le caractère 'A'
.
Supprimer des caractères dans une chaîne¶
Rappelons qu’une chaîne étant immuable, on ne va pas strictement supprimer des caractères d’une chaîne.
Remplacer des caractères d’une chaîne s
par la chaîne vide ''
revient à supprimer ces caractères de s
. Le remplacement est effectué à l’aide de la méthode replace
.
Par exemple, soit la chaîne s
suivante
Mars Jupiter Uranus Neptune Pluton
contenant des mots séparés par des blancs de longueurs variables. On souhaite supprimer tous des espaces de s
. Pour cela il suffit de « remplacer » chaque caractère espace par une chaîne vide :
1 2 3 4 5 | s="Mars Jupiter Uranus Neptune Pluton"
espace = " "
t= s.replace(espace, "")
print(s)
print(t)
|
6 7 | Mars Jupiter Uranus Neptune Pluton
MarsJupiterUranusNeptunePluton
|
- Ligne 7 : les espaces ont bien été supprimés
- Lignes 6 et 7 : La chaîne sans espace est une nouvelle chaîne
t
, la chaîne initiales
est inchangée. - La chaîne vide
""
a permis d’écraser les caractères" "
.
Transformer une chaîne par conversion en liste¶
On ne peut pas directement modifier une chaîne puisqu’une chaîne est de type immuable, par exemple on ne peut pas directement changer dans la chaîne "taTou"
la lettre "T"
en le caractère "B"
.
Néanmoins, une liste étant mutable, on peut utiliser la stratégie suivante pour simuler une modification de chaîne s
:
- créer avec le constructeur
list
une listeL
des caractères de la chaîne initiales
- modifier certains éléments de la liste
L
- reconstituer avec la methode
join
une nouvelle chaînet
à partir de la nouvelle liste de caractères.
Illustration :
s = "taTou"
print(s)
print()
L =list(s)
print(L)
print()
L[2]='B'
print(''.join(L))
taTou
['t', 'a', 'T', 'o', 'u']
taBou
Les méthodes partition
et rpartition
¶
La méthode partition
est une version locale de la méthode split
. Le plus simple est de voir un exemple :
1 2 3 4 5 6 7 8 | s = "abcd!_!xyz!_!XYZ"
print(s)
avant, sep, apres = s.partition("!_!")
print("avant :", avant)
print("sep :", sep)
print("apres :", apres)
|
9 10 11 12 | abcd!_!xyz!_!XYZ
avant : abcd
sep : !_!
apres : xyz!_!XYZ
|
ligne 4 : l’appel va partager la chaîne
s
en trois tronçons :- ce qui est à gauche du séparateur donné (ici
!_!
), - ce qui le séparateur lui-même
- ce qui est à droite du séparateur.
- ce qui est à gauche du séparateur donné (ici
La partition se fait suivant la première occurrence du séparateur, autrement dit l’occurrence la plus à gauche.
Ci-dessous, le comportement de partition
si le séparateur n’est pas dans la chaîne :
1 2 3 4 5 6 7 | s = "abcd!_!xyz!_!XYZ"
avant, sep, apres = s.partition("123")
print("avant :", avant)
print("sep :", sep)
print("apres :", apres)
|
8 9 10 | avant : abcd!_!xyz!_!XYZ
sep :
apres :
|
La méthode rpartition
¶
La méthode rpartition
est analogue à partition
sauf que la partition s’effectue suivant l’occurrence du séparateur la plus à droite. Voici un exemple :
s = "abcd!_!xyz!_!XYZ"
avant, sep, apres = s.rpartition("!_!")
print("avant :", avant)
print("sep :", sep)
print("apres :", apres)
avant : abcd!_!xyz
sep : !_!
apres : XYZ
La méthode rsplit
¶
La méthode rsplit
est analogue à la méthode split
en l’absence d’utilisation de l’option maxsplit
:
numero = "08-42-20-32-17"
L = numero.rsplit("-")
M = numero.split("-")
print(L == M)
True
En revanche, si l’option maxsplit
est utilisée, les occurrences du séparateur sont retirées en commençant par la droite (d’où le r pour right) au lieu de par la gauche comme c’est le cas pour split
:
numero = "08-42-20-32-17"
L = numero.split("-", maxsplit=2)
print(L)
M = numero.rsplit("-", maxsplit=2)
print(M)
['08', '42', '20-32-17']
['08-42-20', '32', '17']
Recherche de sous-chaînes avec in
¶
Soit la chaîne
s = "xyxABCyyyxABCxyyxABCxyx"
La chaîne 'ABC'
est une sous-chaîne de s
et elle y apparaît même 3 fois. Rechercher une sous-chaîne dans une chaîne est une opération classique en programmation et appelée recherche de motif, en anglais pattern matching. Les caractères de la sous-chaîne doivent être contigus et non séparés ; ainsi la chaîne ABC
n’est pas considérée comme étant une sous-chaine de la chaîne xxxAxxxxBxxCxxxx
.
Une première possibilité de détection d’une sous-chaîne est d’utiliser l’opérateur in
qui va renvoyer True
ou False
selon la présence ou l’absence de la sous-chaîne :
s = "xyxABCyyyxABCxyyxABCxyx"
print("ABC" in s)
print("AbC" in s)
True
False
En première approximation, voici comment se fait la recherche de la sous-chaîne : elle se poursuit jusqu’à obtention d’une occurrence de la sous-chaîne ou alors lorsque la fin de la chaîne est atteinte. Si une occurrence est découverte, la recherche s’interrompt.
Pour entrer dans les détails d’implémentation en CPython, il semble que l’opérateur in
fasse appel aux algorithmes de Boyer-Moore et Horspool. L’examen du code-source de la fonction PyUnicode_Contains
montre que des fonctions spécialisées de recherche sont invoquées, par exemple ucs1lib_find
, qui font appel à la fonction FASTSEARCH
.
Recherche de sous-chaînes avec find
, rfind
, index
et rindex
¶
L’opérateur in
indique si oui ou non, une sous-chaîne est présente dans une chaîne mais, si elle est présente, il ne fournit aucun information sur sa position. C’est là que les méthodes find
et index
sont utiles : elles donnent l’indice de la 1re position d’une sous-chaîne dans une chaîne donnée.
Voici un exemple illustrant l’usage de find
:
1 2 3 4 5 6 7 | s = "xyyxxABCyyyxABCxyyxABCxyx"
p = s.find("DEF")
print(p)
p = s.find("ABC")
print(p)
|
8 9 | -1
5
|
- Lignes 3 et 8 : si la sous-chaîne est absente, l’entier
-1
est renvoyé. - Lignes 6 et 9 : si la chaîne est présente, l’indice du début de la première occurrence de la sous-chaîne est renvoyé. Ici, c’est 5 et, en effet, aux indices 5, 6 et 7 de la chaîne
s
, on litA
,B
etC
.
La méthode index
est équivalente à la méthode find
sauf qu’elle lève une exception ValueError
lorsque la sous-chaîne est absente, ce qui peut être intimidant si vous ne connaissez pas les exceptions en Python. Voici un exemple :
1 2 3 4 5 6 7 8 9 | s = "xyyxxABCyyyxABCxyyxABCxyx"
p = s.index("ABC")
print(p)
print()
p = s.index("DEF")
print(p)
|
10 11 12 13 14 15 | 5
Traceback (most recent call last):
File "code.py", line 8, in <module>
p = s.index("DEF")
ValueError: substring not found
|
- Lignes 3-4 et 10 : l’indice du début de la séquence est trouvé.
- Lignes 8 et 12-15 : indice non trouvé donc levée d’une exception et fin du programme.
On pourrait gérer l’exception :
s = "xyyxxABCyyyxABCxyyxABCxyx"
for sub in ["ABC", "DEF"]:
try:
p = s.index(sub)
print(p)
print()
except ValueError:
print(f'"{sub}" absent de "{s}"')
5
"DEF" absent de "xyyxxABCyyyxABCxyyxABCxyx"
L’implémentation des méthodes find
et index
est basée sur l’algorithme de Boyer-Moore-Horspool.
Paramètres de la méthode find
¶
Les méthodes find
et index
disposent de paramètres optionnels permettant de fenêtrer la recherche. Regardons pour find
:
1 2 3 4 5 6 7 | s = "xyyxxABCyyyxABCxyyxABCxyx"
p = s.find("ABC", 10)
print(s[p:])
p = s.find("ABC", 10, 20)
print(s[p:])
|
8 9 10 11 | 12
ABCxyyxABCxyx
12
ABCxyyxABCxyx
|
- Ligne 3 : recherche le premier indice de la chaîne
s
à partir de l’indice 10 où pourrait se trouver la chaîneABC
- Ligne 7 : recherche le premier indice de la chaîne
s
, entre les incides 10 (inclus) et 20 (exclu) où pourrait se trouver la chaîneABC
Les méthodes rfind
et rindex
¶
Un appel s.find(t, i, j)
cherche le plus petit indice de la chaîne s
, situé entre l’indice i
et l’indice j
(non compris) où pourrait commencer la chaîne t
et donc find
cherche l’occurrence de t
dans s
qui soit la plus à gauche dans la plage à tester.
La méthode rfind
fait la même chose mais recherche l’occurrence la plus à droite (d’où le rfind
pour right). Voici un exemple :
1 2 3 4 5 | s = "xyyxxABCyyyxABCxyyxABCxyx"
print(s[10:25])
p = s.rfind("ABC", 10, 25)
print(p)
print(s[p:])
|
6 7 8 | yxABCxyyxABCxyx
19
ABCxyx
|
- Lignes 2-3 : on cherche la présence de la chaîne
ABC
danss
entre les indices 10 et 25, donc dans la sous-chaîne de la ligne 6. - lignes 3-5 : l’indice le plus à droite (le plus grand donc) où commence
ABC
est 19 et ``ABC``se lit au début de la sous-chaîne ligne 8.
La méthode rindex
est analogue sauf qu’en cas d’échec de recherche, elle lève une exception au lieu de renvoyer -1.
Méthode count
¶
La méthode de chaîne count
permet de compter le nombre d’occurrences d’une sous-chaîne dans une chaîne :
s = "xyyxxABCyyyxABCxyyxABCxyx"
sub= "ABC"
N = s.count(sub)
print(N)
3
On notera que la méthode count
entraîne le parcours caractère par caractère de la chaîne à examiner. L’implémentation est basée sur l’algorithme de Boyer-Moore-Horspool.
Sous-chaînes et chevauchement¶
Testons le nombre d’occurrences dans la chaîne vvvABABAvvv
de la sous-chaîne ABA
:
s = "vvvABABAvvv"
sub= "ABA"
N = s.count(sub)
print(N)
1
On pourrait considérer que la sous-chaîne est présente deux fois dans la chaîne puisque dans ABABA
, la sous-chaîne ABA
apparaît
- aux trois premières positions
- aux trois dernières.
Néanmoins, les fonctions Python de recherche de toutes les sous-chaînes effectuent toujours une lecture de la chaîne dans le sens gauche-droite et reprennent leur recherche immédiatement après la fin de la sous-chaîne trouvée. Ainsi, la méthode Python count
ne comptera qu’une seule occurrence de la sous-chaîne ABA
dans la chaîne vvvABABAvvv
, et non deux.
Effacer les blancs aux extrémités d’une chaîne¶
On a parfois besoin de « normaliser » une chaîne de caractères en omettant les blancs qu’elle contient au début ou à la fin. Par exemple, soit la chaîne z
présente dans le code ci-dessous
z = " oui "
print("Réponse : ...", z, "...")
ce qui affiche
Réponse : ... oui ...
On voudrait « éliminer » de la chaîne z
les espaces en début et fin de chaîne et récupérer dans une nouvelle chaîne la sous-chaîne de contenu oui
.
Pour cela, on utilise la méthode strip
:
z = " oui "
zz = z.strip()
print("Réponse : ...", z, "...")
print("Réponse : ...", zz, "...")
Réponse : ... oui ...
Réponse : ... oui ...
La méthode strip
appliquée à une chaîne z
renvoie une nouvelle chaîne obtenue en supprimant tous les blancs initiaux et terminaux présents dans la chaîne z
. Par blancs, il faut comprendre les espaces, tabulations et/ou sauts de ligne.
Suppression à droite ou à gauche¶
Pour supprimer uniquement les espaces en début d’une chaîne z
, on applique à z
la méthode standard lstrip
:
z = " oui "
zz = z.lstrip()
print("Réponse : ...", z, "...")
print("Réponse : ...", zz, "...")
Réponse : ... oui ...
Réponse : ... oui ...
L’initiale l
dans lstrip
fait référence à left strip.
De même, pour supprimer les espaces en fin de chaîne, on utilise la méthode rstrip
:
z = " oui "
zz = z.rstrip()
print("Réponse : ...", z, "...")
print("Réponse : ...", zz, "...")
Réponse : ... oui ...
Réponse : ... oui ...
L’initiale r dans rstrip
fait référence à right strip.
Identification du début et fin de chaîne¶
On veut savoir si une chaîne donnée se termine, par exemple, par le suffixe pdf. On utilise pour cela la méthode endswith
:
nomDeFichier="mon_doc.pdf"
is_pdf = nomDeFichier.endswith("pdf")
print(is_pdf)
True
Noter que le nom de la méthode contient un s
et que ends
et with
ne sont pas séparés par un blanc souligné _
comme c’est le cas pour certaines méthodes, par exemple is_integer
.
Il est même possible de proposer plusieurs chaînes placées dans un tuple (et pas une liste) pour tester plusieurs terminaisons à examiner :
s = "my_code.py"
is_code = s.endswith(("c", "cpp", "pyx", "py"))
print(is_code)
s = "my_code.txt"
is_code = s.endswith(("c", "cpp", "pyx", "py"))
print(is_code)
True
False
De même, la méthode startswith
permet de savoir si une chaîne commence par une chaîne donnée :
s ="pypy"
is_py = s.startswith("py")
print(is_py)
s ="Python"
is_py = s.startswith(("py", "Py"))
print(is_py)
True
True
Modification du début et fin de chaîne¶
On peut retirer de la fin d’une chaîne des caractères figurant dans une liste donnée. Pour cela on utilise la méthode rstrip
(r
pour right). Par exemple, supposons que l’on veuille supprimer de la fin d’une chaîne des signes de pontuation et les espaces :
s="Bien sûr ... !????"
print(s)
t=s.rstrip("?!, ;.")
print(t)
Bien sûr ... !????
Bien sûr
La méthode lstrip
procède de même mais en début de chaîne :
s="-00042"
print(s)
t=s.lstrip("+-0")
print(t)
-00042
42
La méthode strip
agit comme si lstrip
et rstrip
étaient appelées successivement :
s="+000.42000"
print(s)
t=s.strip("+-.0")
print(t)
+000.42000
42
Il est également possible, à partir de Python 3.9, de supprimer un préfixe donné du début ou de la fin d’une chaîne. Pour cela, on utilise la méthode removeprefix
. Par exemple, soit à supprimer le préfixe poly
:
1 2 3 4 | L = ["polytechnique", "polygone", "polymorphisme", "Polyvalent"]
for s in L:
t=s.removeprefix("poly")
print(f"{s: <14} : {t}")
|
5 6 7 8 | polytechnique : technique
polygone : gone
polymorphisme : morphisme
Polyvalent : Polyvalent
|
De même, on peut supprimer des suffixes avec la méthode removesuffix
.