Streamlit: Créer des apps en Python très simplement
Tags: python, streamlit, french, programming
Published: 2024-05-27 19:48:27

Depuis quelques temps, je me suis mis à l'apprentissage du vietnamien. Pour cela, comme beaucoup de monde en ce moment, je me suis inscrit sur l'application Duolingo. Cette application est vraiment pas mal, elle rend l'apprentissage rapide et ludique.

Oui mais voilà, comme beaucoup d'applications de nos jours, Duolingo propose une offre Freemium, et son plein potentiel n'est libéré qu'avec un abonnement. Notamment, une des fonctionnalités intéressantes qu'offre l'abonnement, c'est la possibilité de faire un simple quiz de traduction vietnamien / anglais avec tous les mots appris depuis le début des leçons. Je me suis alors demandé "Comment essayer de reproduire cet exercice, finalement assez simple, pour pouvoir pratiquer sans avoir à payer un abonnement Duolingo ?"

Dans cet article, je vais essayer de vous montrer comment recréer cet exercice avec le framework Streamlit et un peu de code.

## D'abord, les données

Il me faut pour commencer un premier dataset pour réaliser ce projet. Ça tombe bien, sur son site internet, Duolingo me liste tous les mots que j'ai appris depuis le début de mes leçons, avec leurs traductions anglaises (je ne peux qu'apprendre le vietnamien à partir de l'anglais depuis l'application).

![Rapide copier/coller](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1w6jjaunh7jv2q96x3lm.gif)

Un rapide copier/coller de la liste de mots dans un fichier texte (désolé Duolingo...), et me voilà avec un petit dataset de 131 mots vietnamiens avec la traduction anglaise correspondante. Reste encore à reformatter le tout avec un rapide script Python, et j'ai un JSON qui contient les données de base de mon futur jeu.

```json
{
	"translations": [
		{
			"VN": "ga",
			"EN": "station, gas"
		},
		{
			"VN": "trạm",
			"EN": "station, stations"
		},
		{
			"VN": "đặt chỗ",
			"EN": "make a reservation, reservation"
		},
		...
```

## Streamlit

Plusieurs choix s'offraient à moi pour créer mon application. Ce que je voulais, c'était avoir quelque chose de relativement simple à implémenter, pouvant être facilement déployé pour pouvoir y accéder partout, et apprendre quelque chose en travaillant sur ce projet. Du coup, quoi faire ? Faire un projet HTML/JS simple, essayer de me plonger dans un framework JS (React ou Vue.js), ou trouver une autre alternative ?

Le projet HTML/JS ? Oui, pourquoi pas, mais pas sûr d'en apprendre quelque chose d'intéressant.

Faire une application via un framework JS ? Oui, ça serait super, mais je connais ce genre de projet pour y avoir déjà goûté plusieurs fois... Ce sont de supers frameworks, mais il faut quand même apprendre à s'en servir (j'ai su faire du React il y a quelques années, mais ça évolue tellement vite qu'aujourd'hui je dois repartir de zéro) pour ne pas faire n'importe quoi, et il faut passer 2 semaines à apprendre un framework pour faire une application de quelques lignes qui prend 4 heures à implémenter.

De plus, je ne suis plus super à l'aise avec JS...

Alors pourquoi ne pas me tourner vers autre chose ? Je suis un pythonista dans mon quotidien, et j'ai entendu parler du framework Streamlit au travail. Il paraît que l'on peut faire des applications sympas avec. Alors, y jeter un coup d'œil :detective:

### Rapide focus sur le Framework 

Allez sur le site de [Streamlit](https://streamlit.io), et vous verrez s'afficher devant vous : "A faster way to build and share data apps". Ça semble être ce que l'on cherche :smile:. Quelques minutes à parcourir la documentation me le confirment. C'est simple à installer, à utiliser, à déployer, et c'est du full Python. Voyons si ça peut faire l'affaire !

### L'installation

L'installation du framework est assez simple et se réalise en quelques commandes :

```bash
mkdir translation-exercice-app
cd translation-exercice-app
# assurez vous d'avoir une version de python >= 3.8
python3 --version 
python3 -m venv .venv
source .venv/bin/activate
pip install streamlit
```

Et c'est tout. Simple, non ? Pour la suite de l'article, la seule commande Streamlit que l'on utilisera sera `streamlit run app.py`.

## C'est parti pour l'implémentation

Avec votre éditeur favori, ouvrez le dossier `translation-exercise-app`. Normalement, votre dossier .venv devrait déjà s'y trouver. Ajoutez le fichier JSON contenant le dataset initialement généré, puis créez un fichier `app.py`.

```python
# app.py 
import streamlit as st

st.text("Hello World!")
```

Maintenant, utilisez la commande `streamlit run app.py`, et une page de votre navigateur devrait s'ouvrir en affichant "Hello World!". Première victoire facile :muscle: !

Je ne vais pas vous expliquer un par un tous les composants Streamlit que je vais utiliser. Je préfère vous mettre les liens vers la documentation officielle, qui est bien meilleure que toutes mes explications pourront l'être. Je vais plutôt vous montrer comment je les utilise pour créer mon app d'exercice de traduction.

### Un peu de design d'abord

Qu'est-ce que je veux faire exactement ?

Je me suis dit que, pour le moment, j'allais faire une app très simple. Son but sera d'afficher un mot en vietnamien pris dans le dataset de base, ainsi que 4 traductions anglaises, une correspondant à l'exacte traduction du mot vietnamien choisie, les 3 autres étant juste des mots anglais pris au hasard dans le reste du dataset. Le joueur devra trouver la bonne traduction anglaise parmi les 4 proposées. On aura un score affiché à l'écran qui comptera le nombre de bonnes réponses d'affilée du joueur. Ce score retombera à 0 en cas d'erreur. On affichera également le meilleur score obtenu.

### Charger les données dans l'app

Pour que l'app fonctionne, on doit lui mettre à disposition le dataset. Alors c'est parti:
```python 
import json

import streamlit as s

# On crée une fonction qui va lire et charger les données dans un dictionnaire Python
def load_data() -> dict:
	data  = {}
	with  open("vn_en_words_translations.json") as  fd:
		data = json.load(fd)
	return data

# On vérifie si les données sont dans le "session_state" (ou cache) de Streamlit
# Si ce n'est pas le cas, alors on utilise la fonction pour charger les données
if "words_dict" not in st.session_state:
	st.session_state["words_dict"] = load_data()
```
Grâce à ce bout de code, je peux charger mes données et les stocker dans le **session state** de Streamlit (j'expliquerai plus tard ce qu'est le session state). C'est assez simple et ça me permet d'accéder à mon dataset partout dans mon fichier via `st.session_state["words_dict"]`, ce qui est assez pratique.

### Création du dataset de quiz

Nous nous attaquons à la partie la plus "difficile" (en réalité, c'est très simple, ne paniquez pas) en termes de logique de notre application. L'idée ici est de créer une fonction qui va sélectionner au hasard un mot vietnamien et 4 mots anglais, dont un sera la traduction de notre mot vietnamien. Je décide pour cela d'utiliser une structure de données simple : un dictionnaire avec deux clés. L'une d'elles est associée à une liste qui contiendra le mot vietnamien (cette liste sera toujours de taille 1). L'autre est associée à une liste qui contiendra les 4 traductions anglaises, en prenant soin de toujours placer la bonne traduction au début de la liste (index 0). 

```python
import random

def  select_quizz_words(words_dict: dict) -> dict:
	# On créer le dictionnaire
	selected_words = {
		"VN": [],
		"EN": []
	}
	# On détermine au hasard un index que l'on utilise pour prendre un mot vietnamien
    # et son équivalent anglais dans notre dataset que l'on place en début de liste.
	selected_word_index = random.randrange(0, len(words_dict["translations"]))
	selected_words["VN"].append(words_dict["translations"][selected_word_index]["VN"])
	selected_words["EN"].append(words_dict["translations"][selected_word_index]["EN"])
    # Ainsi, selected_words["EN"][0] sera toujours la bonne traduction de selected_words["VN"][0]
```
Nous continuons ensuite notre fonction pour choisir les trois autres mots anglais au hasard, en nous assurant que ces mots remplissent les deux conditions suivantes :

- Ils ne doivent pas correspondre à `selected_words["EN"][0]`, sinon la bonne réponse apparaîtra en doublon dans nos 4 propositions.
- Il ne doit pas y avoir de doublons dans nos 4 propositions.

```python
import random

def  select_quizz_words(words_dict: dict) -> dict:
	selected_words = {
		"VN": [],
		"EN": []
	}
	selected_word_index = random.randrange(0, len(words_dict["translations"]))
	selected_words["VN"].append(words_dict["translations"][selected_word_index]["VN"])
	selected_words["EN"].append(words_dict["translations"][selected_word_index]["EN"])
	
	find_other_words = True
    # On boucle tant que 3 autres mots n'ont pas été choisis
	while find_other_words:
        # On détermine au hasard un index que l'on utilise pour prendre mot anglais dans notre dataset
		selected_en_word_index = random.randrange(0, len(words_dict["translations"]))
        # Bien sûr, ce mot ne doit pas être celui choisi plus haut
		if selected_en_word_index != selected_word_index:
            # Ni déjà être dans notre liste
			if words_dict["translations"][selected_en_word_index]["EN"] not in selected_words["EN"]:
                # Si les deux conditions sont remplies, alors on l'ajoute à notre liste
				selected_words["EN"].append(words_dict["translations"][selected_en_word_index]["EN"])
		if len(selected_words["EN"]) == 4:
			find_other_words = False
	return selected_words
```

Nous avons maintenant les données pour notre quiz. 

### Le contenue de l'app

Alors c'est bien joli tout ça, mais pour le moment notre application ressemble toujours à une page blanche qui dit bonjour. Il serait temps d'y mettre du contenu ! Commençons par y mettre deux ou trois phrases qui expliquent au joueur ce qu'il fait là.

```python 
selected_words_dict = select_quizz_words(words_dict=st.session_state["words_dict"])

st.title("Hello Learners :wave:!")
st.subheader("Let's make a small game. I give you vietnamese word, and you try to give me the good english translation. Let's go?")

st.write(f"What is the english translation of the word **{selected_words_dict['VN'][0]}**?")
```

C'est un début. D'abord, nous chargeons nos données de quiz dans une variable globale appelée `selected_words_dict` en utilisant la fonction écrite plus haut. Ensuite, nous expliquons rapidement les règles en affichant du texte très simplement via `st.title()` et `st.subheader()`, et nous proposons le mot vietnamien à traduire via `st.write()`. Vous pouvez trouver tous les détails des fonctions Streamlit qui permettent d'afficher du texte à l'écran [ici](https://docs.streamlit.io/develop/api-reference/text). 

Venons-en maintenant à proposer à l'utilisateur de l'application les 4 propositions de traduction en anglais. Il faut trouver un moyen de permettre au joueur d'interagir avec notre application en faisant un choix. J'ai décidé ici d'utiliser le composant Streamlit [`st.button()`](https://docs.streamlit.io/develop/api-reference/widgets/st.button). Je vais afficher 4 boutons côte à côte horizontalement, chacun affichant une des 4 réponses possibles. L'utilisateur pourra alors choisir en cliquant sur l'un d'entre eux. Voici le code pour implémenter ces boutons :

```python
# On 'copie' notre liste de propositions en anglais puis on la mélange comme un bon cocktail
selected_en_words_dict_shuffled = selected_words_dict["EN"].copy()
random.shuffle(selected_en_words_dict_shuffled)
# On crée un layout Streamlit composé de 4 colonnes.
col1, col2, col3, col4  =  st.columns(4)
# Et pour chacun d'entre eux, on y insère un bouton cliquable contenant la proposition
with col1:
	word = selected_en_words_dict_shuffled[0]
	st.button(label=word, key=word, on_click=check_result, args=[word], use_container_width=True)
with col2:
	word = selected_en_words_dict_shuffled[1]
	st.button(label=word, key=word, on_click=check_result, args=[word], use_container_width=True)
with col3:
	word = selected_en_words_dict_shuffled[2]
	st.button(label=word, key=word, on_click=check_result, args=[word], use_container_width=True)
with col4:
	word = selected_en_words_dict_shuffled[3]
	st.button(label=word, key=word, on_click=check_result, args=[word], use_container_width=True)
```
J'ai décidé de mélanger mon tableau de réponses, car sinon, rappelez-vous, nous nous retrouverions avec la bonne réponse toujours en première position, ce qui enlèverait pas mal de suspense à l'exercice...

Ensuite, nous utilisons le système de [layout](https://docs.streamlit.io/develop/api-reference/layout) de Streamlit pour créer 4 colonnes. Pour chacune d'entre elles, nous ajoutons un bouton cliquable représentant une des réponses. À noter que :
- `label` est le texte du bouton.
- `key` est une clé utilisée par Streamlit pour avoir des boutons uniques.
- `on_click` est une méthode de callback dont nous parlerons juste après.
- `args` est un tableau de paramètres qui sera donné à la méthode de callback. Nous lui passons donc la proposition du bouton pour valider ou non le clic. 
- `use_container_width` est un simple booléen permettant au bouton de savoir qu'il peut prendre toute la largeur du conteneur dans lequel il est, ici la colonne (et je n'ai pas eu besoin de faire 5h de CSS pour réussir ça, ce qui est un miracle :pray:).

Cette partie du code peut sûrement être factorisée, mais je n'ai pas trouvé de moyen correct de le faire pour le moment. Ce sera peut-être l'objet d'un futur article.

### Valider ou non le résultat

Notre application commence à avoir du contenu. Reste maintenant à valider le résultat du quiz. Comme énoncé plus haut, on va permettre au joueur de voir un score. On va aussi lui permettre de voir son meilleur score. Alors commençons par définir ces deux variables : 
```python
#... 
if  "words_dict"  not  in  st.session_state:
	st.session_state["words_dict"] = load_data()

if "score" not in st.session_state:
	st.session_state["score"] = 0

if "best_score" not in st.session_state:
	st.session_state["best_score"] = 0
# ...
``` 
Ensuite, attardons-nous sur la méthode de callback `check_result` passée plus haut à nos boutons :
```python
def check_result(choice):
	if  choice == selected_words_dict["EN"][0]:
		st.session_state["score"] += 1
	else:
		if  st.session_state["score"] > st.session_state["best_score"]:
			st.session_state["best_score"] = st.session_state["score"]
		st.session_state["score"] =  0
```
Cette méthode est très simple. Si le choix de notre utilisateur correspond au premier élément de notre liste de propositions anglaises, alors c'est le jackpot, et on incrémente son score. Sinon, on met à jour le meilleur score si ce dernier est strictement inférieur au score actuel, puis on met ce dernier à 0.

Il ne nous reste plus qu'à afficher nos scores :
```python
st.subheader(body=f"Your current score is: {st.session_state['score']}")
st.subheader(body=f"Your best score is: {st.session_state['best_score']}")
```

Et voila !

### Il est temps de tester 

En utilisant `streamlit run app.py`, on peut rapidement lancer et tester notre application.

![Test de notre application](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z8q5o9pppu8grzapkocc.gif)

Alors oui, ce n'est pas très joli, mais c'est quand même présentable, et cela sans une seule ligne de CSS. On se retrouve avec un petit jeu qui nous permet de travailler un peu notre vietnamien. L'objectif que l'on s'était fixé semble atteint ! Youpi :tada:!

### Qu'est ce qu'il se passe sous le capeau ? 

Il me semble important à ce niveau d'insister sur un mécanisme de Streamlit qu'il faut que tout le monde garde à l'esprit si vous voulez utiliser ce framework. Streamlit recharge ce petit script Python que nous venons d'implémenter à **CHAQUE** interaction que nous avons avec l'application. Dans notre cas, cela veut dire qu'à chaque fois que l'utilisateur clique sur un bouton, c'est tout le script qui est relancé. Cela explique plusieurs choses...

Cela explique pourquoi certaines de mes "variables", comme par exemple le dataset initial, sont chargées de cette manière :
```python 
if "words_dict" not in st.session_state:
	st.session_state["words_dict"] = load_data()
```
Ici, je joue avec le système de ["cache"](https://docs.streamlit.io/get-started/fundamentals/advanced-concepts#caching) de Streamlit pour éviter d'avoir ces données rechargées à chaque interaction. C'est aussi ce qui me permet de conserver mon score malgré les rechargements successifs. En effet, à chaque re-run du script, Streamlit teste si le dictionnaire `st.session_state` contient un élément associé à `"words_dict"` (ce qui est le cas) et donc ne le recharge pas.

Cela explique enfin pourquoi la plupart des variables et instructions Streamlit sont réalisées au niveau du scope global du script. Par exemple, les propositions utilisées à chaque question sont chargées dans le scope global :
``` python 
selected_words_dict = select_quizz_words(words_dict=st.session_state["words_dict"])
```
Je procède de cette manière car j'ai besoin que ce dictionnaire soit recréé à chaque fois, histoire de changer les propositions et de rendre le jeu, disons... intéressant ! Les boutons sont ainsi recréés aussi à chaque fois avec de nouvelles valeurs, ce qui est notre objectif.

## En conclusion

Grâce à Streamlit, j'ai réussi à implémenter cette petite application en moins de 100 lignes de code et en une poignée d'heures. Alors certes, je ne pars pas de rien en Python, et l'application est loin d'être parfaite, tant sur le plan technique que visuel, mais je suis assez content du résultat. Je suis très agréablement surpris de la facilité d'utilisation du framework et du résultat.

Le fait que le script soit entièrement rechargé à chaque fois me fait penser que le framework doit avoir des limitations pour des applications plus complexes. De plus, les composants disponibles, bien que nombreux, ne couvrent pas tous les cas d'utilisation d'applications plus sophistiquées. Mais pour des petits prototypes et des applications simples comme celle présentée dans cet article, ça fait largement l'affaire.

Concernant mon application, j'aimerais apporter quelques améliorations dans le futur :

- Ajouter un message vert en cas de bon résultat ou rouge en cas d'erreur, expliquant la bonne réponse.
- Permettre à l'utilisateur de choisir le sens de l'exercice : du vietnamien à l'anglais ou de l'anglais au vietnamien.
- Permettre à l'utilisateur de choisir entre un exercice de traduction de mots ou de phrases.
- Déployer mon application en ligne.
- Faire quelques factorisations du code.
- Ajouter d'autres fonctionnalités qui me viendront en tête plus tard, je l'espère :thinking:.

Cela fera sûrement l'objet de futurs articles.

Ceci étant dit, je vais continuer à explorer ce framework qui répond à l'une de mes problématiques : réussir à créer des applications simples en Python sans avoir besoin de passer plusieurs jours à apprendre à utiliser un framework complexe.

À bientôt pour de nouvelles aventures dans le monde simple et efficace de Streamlit. N'hésitez pas à consulter le [repo](https://github.com/kevinl75/practice-vietnamese-app) Gitlab pour retrouver le code de l'article.

PS: Cet article a été corrigé grâce à L'IA.