Sous-chaînes, transformations

\(\newcommand{\ds}{\displaystyle}\) \(\newcommand{\Frac}{\ds\frac}\) \(\renewcommand{\r}{\mathbb{ R}}\) \(\newcommand{\C}{\mathbb{ C}}\) \(\newcommand{\n}{\mathbb{ N}}\) \(\newcommand{\z}{\mathbb{ Z}}\) \(\newcommand{\Q}{\mathbb{ Q}}\) \(\newcommand{\N}{\mathbb{ N}}\) \(\newcommand{\n}{\mathbb{ N}}\) \(\newcommand{\ol}{\overline}\) \(\newcommand{\abs}[1]{\left| \,{#1} \right|}\) \(\newcommand{\pv}{\;;\;}\) \(\newcommand{\ens}[1]{\left\{ {#1} \right\}}\) \(\newcommand{\mens}[1]{\setminus\left\{ {#1} \right\}}\) \(\newcommand{\Par}[1]{\left({#1}\right)}\) \(\newcommand{\pe}[1]{\left\lfloor {#1} \right\rfloor}\) \(\newcommand{\trans}[1]{\,^t\!{#1}}\)

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îne s 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 de print est la sortie standard stdout (en clair, dans la console de texte) et ici il a été changé en s.
  • Ligne 7 : pour récupérer le contenu de la chaîne s, il faut appeler la méthode getvalue.

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 suite XY de z est remplacée par ZZZ.
  • Lignes 3 et 5 : La chaîne originale z est préservée ie les blocs XY 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 initiale s 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 liste L des caractères de la chaîne initiale s
  • modifier certains éléments de la liste L
  • reconstituer avec la methode join une nouvelle chaîne t à 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.

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 lit A, B et C.

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îne ABC
  • 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îne ABC

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 dans s 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.