<jupyter_start><jupyter_text>Python: les fondamentaux  Données prédéfinies, types et opérations basiques  Types simples: `str` - `int` - `float` - `bool` - `tuple` - `NoneType` Tous ces types sont **immuables** (*immutable*)  Chaînes de caractères - *string* - `str` - `".."` ou `'..'`<jupyter_code>bonjour = "Salut à tous!" # ou 'Salut à tous'
bonjour[0], bonjour[1], bonjour[2], bonjour[-1] # -> 'S', 'a', '!'
bonjour.upper(), bonjour.lower(), bonjour.split() # -> 'SALUT À TOUS!', 'salut à tous!', ['Salut', 'à', 'tous!']
bonjour + " et toutes!" # -> 'Salut à tous! et toutes!'
type(bonjour), len(bonjour) # -> str, 13<jupyter_output><empty_output><jupyter_text>Numériques: entiers `int` et flottant `float`<jupyter_code>a = 1 # type int
b = -1.0 # type float
c = 1.9e-5 # float en notation scientifique 1,9x10^(-5)
a * b + c # float
int(c), float(a) # -> 0, 1.0<jupyter_output><empty_output><jupyter_text>Logique: booléens `bool` - `True` ou `False`<jupyter_code>ok = False
pas_ok = not ok # -> True
type(ok) # -> bool<jupyter_output><empty_output><jupyter_text>n-uplet ou `tuple` - `(, , ...)`<jupyter_code>vide = ()
position = (-2, 5) # ou position = -2, 5
x, y = position
x, y # -> (-2, 5)
type(position), len(position) # -> tuple, 2<jupyter_output><empty_output><jupyter_text>Absence de valeur: `NoneType` - `None`<jupyter_code>reponse = None<jupyter_output><empty_output><jupyter_text>Types construits: `list` - `dict` - `set` Tous ces types sont **muables** (*mutable*) - voir partie «*Affectation*»  Les listes (ou tableaux) `list` - `[, , ...]` et les `range` Regroupe des données de type arbitraire appelées **items** de la liste.Chaque *item* possède un numéro d'ordre appelé **index** ou indice: le premier a l'index 0, le second l'index 1 etc.<jupyter_code>ds = ["un", 2, 3.0, False, None, [1, "deux"]]
ds[0], ds[3], ds[-1], ds[-1][1] # -> "un", False, [1, "deux"] et "deux"
len(ds) # -> 6
vide = [] # liste vide
vide.append("truc") # vide est modifiée et vaut ['truc']
autre = vide + ['bidulle'] # vide n'est pas modifiée et autre vaut ['truc', 'bidulle']<jupyter_output><empty_output><jupyter_text>Ajoutons à cette catégorie les `range` qui sont des listes optimisées; `range` attend 1, 2 ou 3 arguments entiers (type `int`). - **très souvent utilisé**: `range(10)` correspond à `[0,1,2,...,9]` (10 premiers entiers); `range(100)` à `[0,1,...,99]` (100 premiers entiers).      *généralisation*: `range(N)` correspond ainsi à `[0,1,..,N-2,N-1]` (`N` premiers entiers); les index des éléments d'une liste de longueur `N`... - **assez souvent**: `range(20,30)` correpond à `[20,21,..,29]` (30-20=10 entiers à partir de 20)   *généralisation*: `range(a,b)` (lire `a` inclus, `b` exclus) correspond à `[a, a+1, ..., b-2, b-1]` (b-a entiers à partir de a). - **moins souvent**: `range(a,b,pas)` correspond à `[a, a+pas, a+2*pas, ...]` et le dernier entier est strictement inférieur à `b`.<jupyter_code>type(range(10)), list(range(10)), list(range(20,30)), list(range(20,30,3))<jupyter_output><empty_output><jupyter_text>Les dictionnaires `dict` - `{: , : , ...}` Regroupe des *paires* `clé: valeur`: - la **clé** doit-être *unique* (dans le dictionnaire) et *immuable*,- la **valeur** associée est une donnée arbitraire (même une liste ou ... un dictionnaire!)<jupyter_code>cvs = {} # dictionnaire vide
cvs["nom"] = "<NAME>"
cvs["age"] = 25
cvs["celibataire"] = False
cvs["sports"] = ["tennis", "course à pied"]
cvs # -> {"nom": "<NAME>", "age": 25, "celibataire": False, "sports": ["tennis, "course à pied"]}
len(cvs) # -> 4<jupyter_output><empty_output><jupyter_text>Les ensembles `set` - `{, , ...}` Regroupe des valeurs *immuables* et qui ne **peuvent pas se répéter**.<jupyter_code>ens_vide = set()
ens = {1,2,1,3}
ens, type(ens) # -> {1, 2, 3}, set
ens.add(2)
ens # -> {1, 2, 3}
ens.remove(1)
ens, type(ens) # -> {2, 3}, set<jupyter_output><empty_output><jupyter_text>Type fonction: `function` - `def ...` ou `lambda ...` Nous les reverrons plus tard ... mais voici deux exemples simples<jupyter_code># forme habituelle de la déclaration
def carre1(x):
    return x ** 2

# «micro-fonction»
carre2 = lambda x: x ** 2

carre1(5), carre2(5)<jupyter_output><empty_output><jupyter_text>les «micros fonctions» ressemblent à celles qu'on trouvent basiquement en mathématique avec la notation flèchée:$$\text{carre2}: x\mapsto x^2$$où $\mapsto$ est remplacé par `:` et $:$ par `= lambda`  Fonctions prédéfinies *built-in* ce sont les fonctions que python connaît par défaut; elles sont *built-in* (construite directement dans Python).Voici les plus utiles: - `str(..)`, `int(..)`, `float(..)`, `bool(..)`, `tuple(..)`, `list(..)`, `dict(..)`, `set(..)`: convertisse la valeur fournie dans le type indiqué par leur nom; on parle de **constructeurs**.- `len()` renvoie le nombre d'items contenus dans l'argument,- `print(..)`: affiche la ou les valeurs fournies en argument,- `input()`: affiche message et attend que l'utilisateur saisisse une réponse; elle renvoie une `str` qui contient la réponse de l'utilisateur,- **à explorer vous-même**: `min(..)`, `max(..)`, `sum(..)`, `round(..)`, `sorted(..)`, `abs(..)`, `format(..)`, `range(..)`, `enumerate(..)`, `type(..)` (les plus courantes au début) ... etc.<jupyter_code># un exemple pour input
message = "Bonjour, quel âge as-tu? "
reponse = input(message)
print(type(reponse))
age = int(reponse)
f'Anniversaire!! tu as maintenant {age+1} ans!'<jupyter_output><empty_output><jupyter_text>Affectation - ` = ` **variable**: mot arbitraire (sans espace); sert à se référer à une donnée. **expression**: tout ce qui produit une *valeur*: - *ex*: `2 + (3 * 5)`; `len("affectation")`, etc. IMPORTANT: il faut bien différencier **expression** (produit une valeur), **instruction** (produit un effet sans nécessairement avoir de valeur) L'instruction d'**affectation** - ` = ` - sert à *mémoriser une valeur* et à s'*y référer* par la suite. *Exemple*:```pythonune_var = len('affectaction')```Cette instruction est lue de **droite à gauche**: 1. L'expression de droite est *évaluée et remplacée* par sa **valeur**,    - 'expr' qui est ici `len('affectation')` est remplacé par sa valeur: `11` 2. cette valeur est *mémorisée* et son emplacement en mémoire est «étiqueté» avec le mot situé à gauche de `=`,    - `11` est placé dans une case mémoire marquée avec 'var', ici le mot `ma_var` 3. si la variable existe déjà, la valeur écrase celle qui était présente en mémoire.    - l'instruction `ma_variable = 0` a pour effet d'écraser la valeur `11` avec la valeur `0` à l'emplacement mémoire `ma_var`   **Type d'une variable**: c'est le type de la valeur à laquelle elle donne accès. *IMPORTANT*: Il est d'usage d'éviter qu'une variable change de type au cours d'un programme.  Compléments  notion de mémoire Penser à la mémoire comme à un meuble composé de **cases**:- «à l'extérieur»: chaque *case* possède un **numéro** (son *adresse*) et peut être **étiquetée**.- «à l'intérieur»: chaque *case* peut *contenir* une **valeur simple**, *une seule*! (ainsi que son type) Plus précisément, il y a deux cas:1. Pour `a = ` la variable se référe **directement** à la valeur situé dans la case étiquetée `a`.<jupyter_code>a = 5
b = a
b = 2
a, b # -> 5, 2 # a n'a pas changé de valeur...<jupyter_output><empty_output><jupyter_text>2. Pour `a = ` la variable se réfère **indirectement** à la valeur; la case `a` contient en fait l'**adresse** (ou *référence*) de la première valeur simple contenu dans 'valeur construite'.   Le cas 2 est le plus délicat à comprendre; voici un exemple:<jupyter_code>a = [1, 2]
b = a
b.append(5)
a, b<jupyter_output><empty_output><jupyter_text>Le résultat s'explique car l'affectation `b = a` **copie le contenu** de la case `a` dans la case `b` ... **Mais** la case `a` contient en fait l'**adresse** de la liste `[1, 2]` et NON *la liste elle-même* (qui ne tient pas dans une case). Ainsi la case `b` contient à présent la **même adresse** et `b.append(5) = 3` ajoute à la **même liste** la valeur 5. Finalement, `a` se réfère encore à cette liste donc `a == [1, 2, 5]` *Pourquoi en est-il ainsi?*: la *taille mémoire* (nombre de cases) de la liste peut être **modifiée** ... car une liste est *muable*!   Multi-affectation l'instruction `a, b = , ` attribut à la variable `a` la valeur 'val1' et à `b` la valeur 'val2'.Cette syntaxe se généralise pourvu qu'il y ait **autant de variables à gauche que de valeurs à droites**  Opérateurs raccourcis `=`: `+=`, `-=`, `*=` ... l'instruction `a += ` est un raccourci pour l'affectation `a = a + `.Cela fonctionne pour tous les opérateurs arithmétiques avec la même logique. **Retenir**: - **incrémenter** une variable signifie lui ajouter 1 - `a = a + 1 ou a += 1`- **décrémenter** une variable signifie lui enlever 1 - `a = a - 1 ou a -= 1`*Note*: lire `a /= 2` par «la valeur de `a` est divisée par 2»  Opérations sur les données Les opérations dépendent du type des données; on peut dire que le **type d'une donnée** caractérise à la fois:- la donnée elle-même,- mais aussi les opérations qu'elle supporte.**En bref**: type == donnée + opérations!  Opérateurs arithmétiques - `+`, `-`, `*`, `/`, `**`, `//`, `%` `**`: exponentiation (puissance); ex: `3 ** 2` vaut `9` `//`: division entière; ex: «dans 32 combien de fois 5?» `32 // 5` vaut `6` `%`: modulo ou reste de la division entière; ex: «Dans la division de 32 par 5 que reste-t-il?» `32 % 5` (se lit 32 modulo 5) vaut 2;   Opérateurs de comparaisons - `==`, `!=`, ``, `>=`, `in` Produisent un booléen: `True` ou `False`.`==` identique ?; `!=` différent ?; `<` strictement inférieur?; etc.` in ` vaut `True` si 'valeur' appartient à la séquence et vaut `False` sinon.<jupyter_code>2 == 3, 2 != 3, 2 < 3, 2 >= 3 # -> False, True, True, False
"onj" in "bonjour" # -> True
"ok" in ["un", "dix", "pok"] # -> False<jupyter_output><empty_output><jupyter_text>Opérateurs logiques - `not`, `and`, `or` Servent à combiner des booléens obtenus avec des comparaisons; ex:l'expression `x % 2 == 0 or x % 5 == 0` vaut `True` si $x$ est divisible par 2 ou par 5; `False` sinon. *Note*: Dans le contexte de ces opérateurs, tout nombre nul, chaîne ou liste ou dictionnaire vide vaut `False`; et `True` si tel n'est pas le cas.  Opérations communes aux *séquences*: chaînes, n-uplets, listes **nombre d'items** - `len(..)`<jupyter_code>len('oh hé hein bon'), len(('truc', 'bidulle', 'machin')), len(['a', 'e', 'i', 'o', 'u']) # 14, 3, 5<jupyter_output><empty_output><jupyter_text>**concaténation** - `+`<jupyter_code>'ah' + ' ' + "bon ?", (1, 2) + (3, 4), [1, 2] + [3, 4] # -> 'ah bon ?', (1,2,3,4), [1, 2, 3, 4]<jupyter_output><empty_output><jupyter_text>concaténation *répétée* - `.. * `<jupyter_code>'a' * 5, (1,2) * 3, [None] * 4 # -> 'aaaaa', (1,2,1,2,1,2), [None, None, None, None]<jupyter_output><empty_output><jupyter_text>**indexation** - `..[index]`<jupyter_code>"undeuxtrois"[5], ('a','b')[1], [True, False, True, False][2] # -> 'x', 'b', True<jupyter_output><empty_output><jupyter_text>**tranches** (*slice*) - `..[index1:index2]`<jupyter_code>"undeuxtrois"[3:5], (6,5,4,3,2,1)[2:], [6,5,4,3,2,1][:4]<jupyter_output><empty_output><jupyter_text>Fonction - `def ...` ou `lambda ...` Algorithme nommé qui **reçoit** des données en argument et **renvoie** un résultat. Dans l'exemple qui suit:- `capital` est le nom de la fonction,- `depot`, `taux` et `n_annee` sont ses **paramètres formels**: ils servent à **recevoir** les données attendues par la fonction,- **corps** de la fonction: code indenté après la ligne `def ...:`,    - **return** indique que le résultat est prêt à être **renvoyé** (ce qui termine immédiatement la fonction)<jupyter_code># exemple: calcul d'intérêts
def capital(depot, taux, n_annee):
    final = depot
    for _ in range(n_annee):
        final = final + final * taux / 100 # ou mieux: final *= 1 + taux/100
    return final<jupyter_output><empty_output><jupyter_text>L'exemple qui précède donne la syntaxe de **définition** de la fonction `capital`.Voici un exemple d'**utilisation** (ne pas confondre définition/utilisation).<jupyter_code># utilisation
d, t, a = 1000, 5, 10
c = capital(d, t, a) # d, t, a sont les *arguments* fournis à la fonction
f"Un dépot de {d}€ placé à un taux de {t}% aura une valeur de {round(c, 2)}€ au bout de {a} ans"<jupyter_output><empty_output><jupyter_text>On dit souvent «**appeler** - *call* - une fonction» au lieu d'«utiliser une fonction».       Portée des variables Les variables déclarées dans le corps d'une fonction sont **locales** à cette fonction. *Ex*:<jupyter_code>a, x = 5, 2

def f(x):
    a = x
    y = x ** 2
    return a
    
print(f(x))
print(a, x)
print(y) # produit une erreur car y n'existe pas dans la portée globale.<jupyter_output><empty_output><jupyter_text>Il y a en fait deux variables `a`, une dans la **portée globale** (en dehors de toute fonction) et l'autre dans la **portée locale** de la fonction `f`.En effet, lorsqu'une fonction est **appelée**, une *zone mémoire* lui est allouée automatiquement:- c'est à **cette** *zone mémoire* que les variables *situées dans le corps de la fonction* se réfèrent.Lorsque la fonction (se) **termine** (`return` ou dernière instruction):- **sa** *zone mémoire* est **détruite** et donc **ses** variables aussi.Pour cette raison, le valeur de la variable `a` situé en dehors du corps de la fonction n'est pas modifié; Néanmoins:  cas des objets muables: `list`, `dict`, ... Pour ces objets, les variables qui s'y réfèrent le font **indirectement**; la valeur qui leur est associé est en fait l'**adresse** des objets.Pour les fonctions, cela à une importance cruciale; ex:<jupyter_code>l = [1, 2]
def f(x):
    x.append(5)
    return x

f(l), l # -> [1,2,5], [1,2,5]<jupyter_output><empty_output><jupyter_text>En fait, `x` reçoit le *contenu* de la case `l`, c'est à dire l'*adresse de la liste* (et non la liste).**Conséquence**: l'instruction `x.append(5)` travaille sur la liste référencée par `x` or c'est la même que celle référencée par `l` qui est donc modifiée en conséquence.C'est très MAUVAIS car la fonction modifie une variable qui lui est extérieur et cela peut avoir des conséquences inattendues.Pour cette raison, on procède souvent à un **clonage** de la liste à l'aide du constructeur `list` ou du slice `..[:]`:<jupyter_code>l = [1, 2]
def f(x):
    y = list(x) # ou y = x[:]
    y.append(5)
    return y

f(l), l # -> [1,2,5], [1,2] ; «l» n'est pas modifiée<jupyter_output><empty_output><jupyter_text>Ici la liste pointée par `l` reste intacte; c'est ce qu'on veut la plupart du temps  Test et branchement - `if` Un **test** est une *expression* dont la *valeur* est `True` ou `False`.**Note**: là où Python attend un test - après `if` ou `elif`, par exemple; si il trouve une expression dont la valeur n'est pas un `bool`, il la convertit automatiquement (en utilisant la fonction `bool()` tout simplement).Voici les algorigrammes des trois cas principaux d'utilisation des branchements conditionnels: ```python instructions avant le if ...if condition:     bloc if     ... instructions après le if ...```      ```python instructions avant le if ...if condition:     bloc if     ...else:     bloc else     ... instructions après le if ...```      ```python instructions avant le if ...if condition1:     bloc if     ...elif condition2:     bloc elif     ...else:     bloc else     ... instructions après le if ...```       Compléments 1. Si le *bloc if* ne contient qu'une instruction, on peut écrire sur une seule ligne:<jupyter_code>age = 15
if age < 18: print("Vous êtes mineurs!")<jupyter_output><empty_output><jupyter_text>2. Il existe un *opérateur ternaire* (expression a trois arguments) de la forme:      ```python   e1 if cond else e2   ```      sa **valeur** est celle de `e1` si `cond` est vraie, celle de `e2` sinon.      J'insiste: `e1 if cond else e2` est une **expression** (possède une valeur); donc on l'utilise souvent à droite d'une affectation.<jupyter_code>j_ai_compris = False # tester puis mettre à True
alors = "passer à l'étape suivante" if j_ai_compris else "relire!"
alors<jupyter_output><empty_output><jupyter_text>Boucles non bornées - `while` Sert à **répéter** une ou plusieurs instructions - corps de boucle - **tant que** la `condition` (ou test) vaut `True`. **syntaxe**:```python instructions avant la bouclewhile condition:     corps de la ...     boucle instructions après la boucle``` Voici l'algorigramme correspondant:<jupyter_code>j_ai_compris = False

while not j_ai_compris: # condition: not j_ai_compris
    print('va falloir relire...')
    c_est_bon = input("T'as compris? (O ou N)")
    if c_est_bon == 'O':
        j_ai_compris = True

print("C'est bien ... t'as compris")<jupyter_output><empty_output><jupyter_text>`break`: instruction qui force la fin de la boucle.`continue`: instruction qui force le retour à la condition; sert à passer au tour de boucle suivant de manière anticipée.<jupyter_code>t_as_compris = False
ma_patience = 5

while not t_as_compris:
    if ma_patience <= 0: break
    print('il y a un cap à passer...')
    c_est_bon = input("t'as compris? (O ou N ou ??)")
    if c_est_bon not in ['O', 'N']:
        if c_est_bon = '??':
            ma_patience -= 2
            continue
        print("c'est quoi cette réponse? tu le fais exprès j'espère...")
        ma_patience -= 3
    if c_est_bon == 'O':
        break
    else:
        ma_patience -= 1
        
    
print("Prof content" if ma_patience > 0 else "Prof désespéré!")<jupyter_output><empty_output><jupyter_text>**DANGER1**: si on oublie de modifier la condition dans le corps de boucle ou qu'on n'utilise pas l'instruction `break`; la boucle peut tourner indéfiniment!**DANGER2** (moins grave): si la condition vaut `False` au départ, le corps de boucle n'est pas exécuté... (code mort)  Boucle bornées - `for` Variante de la boucle `while` avec l'assurance que la boucle se termine.En pratique: sert à *parcourir* un ensemble de valeurs fixées au départ. **syntaxe**```python instructions avant la bouclefor variable in sequence:     corps de la ...     boucle instructions après la boucle``` - `variable` est une variable qu'on peut nommer comme on veut; à chaque tour de boucle elle prend une nouvelle valeur de `sequence`,- `sequence` est une expression dont la valeur est une `str`, un `range`, un `tuple`, une `list`, un `dict`, un `set`, ...; **bref**: tout type qui peut renfermer plusieurs *items*. Voici son algorigramme:<jupyter_code>p = lambda x: print(x, end=',') # pour afficher sans retour à la ligne.
for c in "abc": p(c) # -> a, b, c,
for n in [1,2,3]: p(n) # -> 1, 2, 3,
for k in {"un": 1, "deux": 2}: p(k) # -> "un", "deux",
for i in range(10): p(i) # -> 0, 1, ..., 9,<jupyter_output><empty_output><jupyter_text>*note*: comme avec `if` et `while`, si le *corps* ne contient qu'une instruction, on peut **la** mettre juste après les `:` Tout ce qu'on peut faire avec un `for` peut être fait avec un `while`; par ex:<jupyter_code># traduction de: for c in "abc": p(c)
i=0
while i < 3:
    print("abc"[i], end=",")
    i = i + 1<jupyter_output><empty_output><jupyter_text>`for` est donc un *raccourci de syntaxe*; cela se justifie par le fait que, la plupart du temps, on utilise une boucle pour parcourir un certain nombre de valeurs; et aussi par le besoin de ne pas se soucier de la terminaison des boucles (voir DANGER1 avec while) Pour terminer cette section, je veux insister sur l'**importance de** `range` pour la boucle `for`:En informatique, pour accéder aux objets, valeurs, ... on se sert (très) **souvent** de leur *adresse*: on y accède alors *indirectement*.Par exemple, si je veux échanger le premier et le dernier item de la liste `["dormir", "manger", "bosser"]` sans produire une nouvelle liste (en mémoire), il est nécessaire d'utiliser les adresses de `"dormir"` et `"bosser"` par rapport à cette liste.L'adresse de `"dormir"` est 0 et celle de `"bosser"` est 2 d'où:<jupyter_code>todo = ["dormir", "manger", "bosser"]
tmp = todo[0]
todo[0] = todo[2]
todo[2] = tmp
todo<jupyter_output><empty_output><jupyter_text>Quel rapport avec `for` et `range`? Et bien, imaginons que j'ai besoin de mettre à 0 tous les nombres pairs d'une liste sans en faire une nouvelle... simple non?On se dit: > parcourons la liste, si le nombre lu est pair, mettons 0 à la place.**Mais**: mettre 0 à la place suppose de connaître son *adresse* (son index) ...<jupyter_code>liste_exemple = list(range(100))
print(liste_exemple)
# for nb in liste_exemple: ... -> ne peut pas fonctionner pour ce pb...
for adr in range(100):
    if liste_exemple[adr] % 2 == 0:
        liste_exemple[adr] = 0
print(liste_exemple)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1576063236506474, 'fraction_numerical': 0.017021482954269918, 'mean_word_length': 4.468575974542562, 'pattern_counts': {'":': 8, '<': 136, '<?xml version=': 0, '>': 165, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '32', 'dir': 'jupyter-structured-clean-dedup', 'id': '533661', 'license': 'CC0-1.0', 'n_tokens_mistral': 7792, 'n_tokens_neox': 7672, 'n_words': 2659, 'path': '00_bases_python/09_cours_python.ipynb', 'repo_name': 'efloti/cours-nsi-premiere', 'size': '20637'}