---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Types de base et expressions

```{code-cell} python
:tags: [hide-input]

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
```

## Les entiers (`int`)

Python dispose d'un type entier remarquable par une propriété fondamentale : les entiers Python sont de **précision arbitraire**. Contrairement aux langages comme C ou Java où les entiers sont limités à 32 ou 64 bits, Python peut représenter et manipuler des entiers aussi grands que la mémoire disponible le permet. Cela le rend particulièrement adapté à la cryptographie, à la théorie des nombres et à tout calcul nécessitant une grande précision.

```{code-cell} python
# Les entiers Python n'ont pas de limite de taille
grand_nombre = 2 ** 1000
print(grand_nombre)
print(f"Nombre de chiffres : {len(str(grand_nombre))}")
```

### Représentations dans différentes bases

Python permet de déclarer des entiers dans différentes bases numériques à l'aide de préfixes dédiés :

```{code-cell} python
# Binaire (base 2) avec le préfixe 0b
entier_binaire = 0b1010_1100
print(f"Binaire 0b10101100 = {entier_binaire} en décimal")

# Octal (base 8) avec le préfixe 0o
entier_octal = 0o755
print(f"Octal 0o755 = {entier_octal} en décimal")

# Hexadécimal (base 16) avec le préfixe 0x
entier_hex = 0xFF_A0
print(f"Hexadécimal 0xFFA0 = {entier_hex} en décimal")

# Conversion vers différentes bases
n = 255
print(f"{n} en binaire : {bin(n)}")
print(f"{n} en octal : {oct(n)}")
print(f"{n} en hexadécimal : {hex(n)}")
```

```{prf:remark}
:label: remark-02-01
Python autorise l'utilisation de tirets bas `_` comme séparateur de groupes dans les littéraux numériques, à la manière de l'espace en mathématiques : `1_000_000` est équivalent à `1000000`. Cette convention améliore la lisibilité des grands nombres sans aucun impact sur leur valeur.
```

### Opérations arithmétiques

```{code-cell} python
a, b = 17, 5

print(f"{a} + {b} = {a + b}")       # Addition
print(f"{a} - {b} = {a - b}")       # Soustraction
print(f"{a} * {b} = {a * b}")       # Multiplication
print(f"{a} / {b} = {a / b}")       # Division (toujours un float)
print(f"{a} // {b} = {a // b}")     # Division entière (quotient)
print(f"{a} % {b} = {a % b}")       # Modulo (reste)
print(f"{a} ** {b} = {a ** b}")     # Puissance
```

```{prf:definition} Division entière et modulo
:label: definition-02-01
L'opérateur `//` effectue la **division entière** : il retourne le quotient de la division euclidienne, arrondi vers le bas (*floor division*). L'opérateur `%` retourne le **reste** de cette division. Ces deux opérateurs vérifient toujours la relation `a == (a // b) * b + (a % b)`. Attention : `//` arrondit vers le bas (et non vers zéro), ce qui affecte le comportement avec les nombres négatifs : `-7 // 2` vaut `-4`, pas `-3`.
```

```{code-cell} python
# Comportement avec les négatifs
print(-7 // 2)   # -4 (arrondi vers le bas)
print(-7 % 2)    # 1  (reste toujours positif avec un diviseur positif)
```

## Les flottants (`float`)

Les flottants Python suivent la norme **IEEE 754** en double précision (64 bits), qui est le standard universel pour la représentation des nombres réels en informatique. Cette norme utilise 1 bit de signe, 11 bits d'exposant et 52 bits de mantisse, ce qui donne environ 15 à 17 chiffres significatifs décimaux.

```{code-cell} python
# Déclaration de flottants
x = 3.14159
y = 2.0
z = 1.5e-3    # Notation scientifique : 0.0015
w = 1_234.567

print(type(x), x)
print(type(z), z)
```

### Précision limitée et erreurs d'arrondi

La représentation en base 2 de certaines fractions décimales est infinie, exactement comme `1/3` est infini en base 10. Cela provoque des **erreurs d'arrondi** inévitables :

```{code-cell} python
# Le fameux problème de précision flottante
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)  # False !

# Solution : utiliser math.isclose pour les comparaisons
import math
print(math.isclose(0.1 + 0.2, 0.3))
print(math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9))
```

```{prf:remark}
:label: remark-02-02
Ne jamais comparer des flottants avec `==`. Utiliser systématiquement `math.isclose(a, b, rel_tol=1e-9)` (tolérance relative) ou `math.isclose(a, b, abs_tol=1e-9)` (tolérance absolue) selon le contexte. La tolérance relative est appropriée pour des nombres de grandeur similaire ; la tolérance absolue est préférable quand l'un des nombres peut être proche de zéro.
```

```{code-cell} python
# round() pour l'affichage (ne résout pas le problème de fond)
print(round(0.1 + 0.2, 10))
print(round(2.675, 2))  # Surprise : vaut 2.67, pas 2.68

# Valeurs spéciales IEEE 754
import math
print(float('inf'))       # Infini positif
print(float('-inf'))      # Infini négatif
print(float('nan'))       # Not a Number
print(math.isinf(1/0))   # False car ZeroDivisionError pour les entiers
print(math.isnan(float('nan')))
```

### Le module `decimal` pour la précision exacte

Quand la précision exacte est indispensable — calculs financiers, comptabilité — le module `decimal` de la bibliothèque standard offre une arithmétique décimale à précision arbitraire :

```{code-cell} python
from decimal import Decimal, getcontext

# Précision par défaut : 28 chiffres significatifs
print(Decimal('0.1') + Decimal('0.2'))
print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3'))

# Augmenter la précision
getcontext().prec = 50
print(Decimal(1) / Decimal(3))
```

## Les booléens (`bool`)

Le type `bool` représente les valeurs de vérité : `True` et `False`. En Python, `bool` est une **sous-classe de `int`** : `True` vaut `1` et `False` vaut `0`, ce qui permet des usages arithmétiques parfois surprenants mais cohérents.

```{code-cell} python
print(type(True))
print(isinstance(True, int))   # True est un int !
print(True + True)             # 2
print(True * 5)                # 5
print(False + 1)               # 1
```

### Opérateurs logiques

```{code-cell} python
# Opérateurs and, or, not
print(True and False)
print(True or False)
print(not True)

# Comparaisons qui retournent des booléens
x = 10
print(x > 5)
print(x == 10)
print(x != 7)
print(3 <= x <= 15)  # Chaînage de comparaisons (pythonique)
```

### Valeurs *falsy* et *truthy*

```{prf:definition} Valeurs falsy et truthy
:label: definition-02-02
En Python, tout objet possède une valeur de vérité. Un objet est dit **falsy** s'il est évalué à `False` dans un contexte booléen. Les valeurs falsy sont : `False`, `None`, `0`, `0.0`, `0j`, la chaîne vide `""`, la liste vide `[]`, le tuple vide `()`, le dictionnaire vide `{}`, l'ensemble vide `set()`, et tout objet dont la méthode `__bool__` retourne `False` ou dont la méthode `__len__` retourne `0`. Tout le reste est **truthy**.
```

```{code-cell} python
# Exemples de valeurs falsy
valeurs_falsy = [False, None, 0, 0.0, "", [], {}, set()]
for v in valeurs_falsy:
    print(f"bool({v!r}) = {bool(v)}")
```

### Court-circuit (*short-circuit evaluation*)

Les opérateurs `and` et `or` utilisent l'évaluation en court-circuit : ils n'évaluent le second opérande que si nécessaire. De plus, ils retournent l'un de leurs opérandes, pas nécessairement un booléen :

```{code-cell} python
# and retourne le premier falsy trouvé, ou le dernier opérande
print(1 and 2)         # 2
print(0 and 2)         # 0
print([] and "hello")  # []

# or retourne le premier truthy trouvé, ou le dernier opérande
print(1 or 2)          # 1
print(0 or 2)          # 2
print([] or "défaut")  # "défaut"

# Usage courant : valeur par défaut
nom = None
affichage = nom or "Anonyme"
print(affichage)
```

## Les chaînes de caractères (`str`)

Les chaînes Python sont des séquences **immuables** de caractères Unicode. Python 3 utilise Unicode (UTF-8) par défaut, ce qui signifie que les chaînes peuvent contenir n'importe quel caractère : lettres accentuées, idéogrammes chinois, émojis, symboles mathématiques.

### Littéraux et formes spéciales

```{code-cell} python
# Différentes façons de déclarer une chaîne
s1 = 'guillemets simples'
s2 = "guillemets doubles"
s3 = """chaîne
sur plusieurs
lignes"""

# Chaîne brute (raw string) : les \ ne sont pas interprétés
chemin = r"C:\Users\alice\Documents"
print(chemin)

# Chaîne d'octets (bytes)
octets = b"données binaires"
print(type(octets))

# Chaîne f (f-string) — le format moderne
prenom = "Alice"
age = 30
print(f"Je m'appelle {prenom} et j'ai {age} ans.")
print(f"Le carré de {age} est {age**2}.")
print(f"Pi ≈ {3.14159:.4f}")   # Formatage avec spécificateur
```

### Méthodes essentielles

```{code-cell} python
texte = "  Bonjour, monde !  "

# Nettoyage
print(repr(texte.strip()))        # Supprime les espaces aux extrémités
print(repr(texte.lstrip()))       # Supprime à gauche seulement
print(repr(texte.rstrip()))       # Supprime à droite seulement

# Transformation de casse
s = "python est génial"
print(s.upper())
print(s.capitalize())
print(s.title())

# Recherche et remplacement
print(s.replace("python", "Python"))
print(s.find("est"))              # Indice de la première occurrence
print(s.count("e"))               # Nombre d'occurrences
print("génial" in s)              # Test d'appartenance
```

```{code-cell} python
# split et join
phrase = "un,deux,trois,quatre"
mots = phrase.split(",")
print(mots)

recombine = " - ".join(mots)
print(recombine)

# startswith et endswith
url = "https://www.example.com"
print(url.startswith("https"))
print(url.endswith(".com"))
```

### Slicing (découpage)

```{code-cell} python
s = "abcdefghij"

print(s[0])       # Premier caractère
print(s[-1])      # Dernier caractère
print(s[2:5])     # Du 3ème au 5ème (exclu)
print(s[:4])      # Les 4 premiers
print(s[6:])      # Du 7ème à la fin
print(s[::2])     # Un sur deux
print(s[::-1])    # Inversé
```

```{prf:remark}
:label: remark-02-03
L'immuabilité des chaînes signifie qu'on ne peut pas modifier un caractère individuel (`s[0] = 'A'` lève une `TypeError`). Pour construire une chaîne modifiée, on utilise les méthodes qui retournent de nouvelles chaînes, ou on passe par une liste de caractères que l'on rejoint ensuite avec `''.join()`. Cette immuabilité est une garantie de sécurité : une chaîne passée à une fonction ne peut jamais être altérée par celle-ci.
```

## `None`

`None` est l'unique instance du type `NoneType`. Il représente l'**absence de valeur** — l'équivalent Python de `null` dans d'autres langages. C'est la valeur de retour implicite des fonctions qui ne retournent rien explicitement.

```{code-cell} python
# None est une valeur sentinelle
def fonction_sans_retour():
    x = 42  # calcule quelque chose mais ne retourne rien

resultat = fonction_sans_retour()
print(resultat)        # None
print(type(resultat))  # <class 'NoneType'>

# None est falsy
print(bool(None))      # False
```

```{prf:definition} Comparaison avec None
:label: definition-02-03
Pour tester si une variable vaut `None`, on utilise **toujours** `is None` (ou `is not None`), jamais `== None`. L'opérateur `is` teste l'**identité d'objet** (même adresse mémoire), tandis que `==` teste l'**égalité de valeur**. Comme `None` est un singleton — il n'existe qu'un seul objet `None` en mémoire — `is None` est à la fois sémantiquement correct et plus performant. Un objet personnalisé pourrait redéfinir `__eq__` pour être égal à `None`, ce qui rendrait `== None` non fiable.
```

```{code-cell} python
valeur = None
if valeur is None:
    print("Aucune valeur définie")

# Usage classique : paramètre optionnel avec None comme sentinelle
def saluer(nom=None):
    if nom is None:
        nom = "inconnu"
    return f"Bonjour, {nom} !"

print(saluer())
print(saluer("Alice"))
```

## Typage dynamique et inférence

Python est un langage à **typage dynamique** : le type d'une variable n'est pas déclaré à l'avance, il est déterminé au moment de l'exécution en fonction de la valeur qui lui est affectée. Une même variable peut changer de type au fil de l'exécution.

```{code-cell} python
x = 42
print(type(x))   # int

x = "bonjour"
print(type(x))   # str

x = [1, 2, 3]
print(type(x))   # list
```

### `isinstance()` et `type()`

```{code-cell} python
n = 42
print(type(n) == int)          # True, mais déconseillé
print(isinstance(n, int))      # True, à préférer
print(isinstance(True, int))   # True : bool est sous-classe de int
print(isinstance(n, (int, float)))  # Test sur plusieurs types
```

```{prf:remark}
:label: remark-02-04
`isinstance()` est généralement préférable à `type() ==` car il tient compte de l'héritage : `isinstance(True, int)` retourne `True`, ce qui est sémantiquement correct. En revanche, `type(True) == int` retourne `False`, ce qui est souvent surprenant. Utiliser `type()` est justifié uniquement quand on veut tester le type exact, sans tenir compte des sous-classes.
```

### *Duck typing*

Python favorise le **duck typing** : *"If it walks like a duck and quacks like a duck, then it's a duck."* On ne vérifie pas le type d'un objet, mais la présence des méthodes ou attributs dont on a besoin. Cela rend le code plus flexible et générique.

```{code-cell} python
# Cette fonction accepte n'importe quel objet itérable
def somme_elements(collection):
    total = 0
    for element in collection:
        total += element
    return total

print(somme_elements([1, 2, 3, 4]))        # Liste
print(somme_elements((10, 20, 30)))        # Tuple
print(somme_elements({100, 200, 300}))     # Ensemble
```

## Conversions

Python distingue les conversions **implicites** (effectuées automatiquement) des conversions **explicites** (demandées par le programmeur).

### Conversions implicites

Python n'effectue que très peu de conversions implicites, et uniquement dans des cas bien définis :

```{code-cell} python
# int + float -> float (promotion automatique)
print(type(3 + 1.5))    # float
print(3 + 1.5)          # 4.5

# bool + int -> int
print(True + 5)         # 6
```

### Conversions explicites

```{code-cell} python
# int() : convertit en entier
print(int(3.9))        # 3 (tronque, ne arrondit pas)
print(int("42"))       # 42
print(int("0xFF", 16)) # 255
print(int("0b1010", 2)) # 10
print(int(True))       # 1

# float() : convertit en flottant
print(float("3.14"))   # 3.14
print(float(42))       # 42.0
print(float("inf"))    # inf

# str() : convertit en chaîne
print(str(42))         # "42"
print(str(3.14))       # "3.14"
print(str(True))       # "True"
print(str(None))       # "None"

# bool() : convertit en booléen
print(bool(0))         # False
print(bool(1))         # True
print(bool(""))        # False
print(bool("hello"))   # True
```

```{prf:remark}
:label: remark-02-05
`int()` **tronque** les flottants vers zéro : `int(3.9)` vaut `3` et `int(-3.9)` vaut `-3`. Pour arrondir à l'entier le plus proche, on utilise `round()`. Pour les conversions invalides (par exemple `int("bonjour")`), Python lève une exception `ValueError` explicite plutôt que de retourner silencieusement une valeur par défaut — conformément à la philosophie *"Errors should never pass silently"*.
```

## Visualisation de la hiérarchie des types numériques

```{code-cell} python
:tags: [hide-input]

fig, ax = plt.subplots(figsize=(10, 7))
ax.set_xlim(0, 10)
ax.set_ylim(0, 9)
ax.axis('off')
ax.set_title("Hiérarchie des types numériques Python", fontsize=15,
             fontweight='bold', pad=20)

palette = sns.color_palette("muted", 4)

noeuds = [
    (5.0, 7.2, "bool\n(True / False)", palette[3], 1.4, 0.7),
    (5.0, 5.2, "int\n(précision arbitraire)", palette[0], 1.8, 0.7),
    (5.0, 3.2, "float\n(IEEE 754, 64 bits)", palette[1], 1.8, 0.7),
    (5.0, 1.2, "complex\n(partie réelle + imaginaire)", palette[2], 2.2, 0.7),
]

for (x, y, label, color, w, h) in noeuds:
    box = patches.FancyBboxPatch(
        (x - w/2, y - h/2), w, h,
        boxstyle="round,pad=0.15",
        linewidth=2,
        edgecolor=color,
        facecolor=color,
        alpha=0.25
    )
    ax.add_patch(box)
    border = patches.FancyBboxPatch(
        (x - w/2, y - h/2), w, h,
        boxstyle="round,pad=0.15",
        linewidth=2,
        edgecolor=color,
        facecolor='none'
    )
    ax.add_patch(border)
    ax.text(x, y, label, ha='center', va='center',
            fontsize=10, fontweight='bold', color=color)

# Flèches de conversion "est un sous-type de"
fleches = [
    (5.0, 6.85, 5.0, 5.55, "sous-classe de"),
    (5.0, 4.85, 5.0, 3.55, "converti vers"),
    (5.0, 2.85, 5.0, 1.55, "converti vers"),
]
for (x1, y1, x2, y2, label) in fleches:
    ax.annotate('', xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle='->', color='#666666', lw=2))
    ax.text((x1 + x2) / 2 + 1.1, (y1 + y2) / 2, label,
            ha='center', va='center', fontsize=8, color='#888888',
            style='italic')

# Exemples de conversions
exemples = [
    (2.0, 7.2, "int(True) → 1\nbool(0) → False"),
    (2.0, 5.2, "float(42) → 42.0\nint(3.7) → 3"),
    (2.0, 3.2, "complex(1.5) → (1.5+0j)"),
]
for (x, y, texte) in exemples:
    ax.text(x, y, texte, ha='center', va='center',
            fontsize=7.5, color='#555555', style='italic',
            bbox=dict(boxstyle='round,pad=0.3', facecolor='#f8f8f8',
                      edgecolor='#cccccc', alpha=0.9))

ax.text(5.0, 8.5,
        "bool ⊂ int  ·  int → float → complex",
        ha='center', va='center', fontsize=10, color='#555555')

plt.tight_layout()
plt.show()
```

## Résumé

Dans ce chapitre, nous avons exploré les types de base de Python :

- Les **entiers** (`int`) sont de précision arbitraire, supportent plusieurs bases (`0b`, `0o`, `0x`) et les opérateurs arithmétiques usuels incluant `//` (division entière), `%` (modulo) et `**` (puissance).
- Les **flottants** (`float`) suivent la norme IEEE 754 en double précision, avec les erreurs d'arrondi inhérentes. On compare toujours des flottants avec `math.isclose()`, jamais avec `==`. Pour la précision exacte, le module `decimal` est la solution appropriée.
- Les **booléens** (`bool`) sont une sous-classe de `int` ; `True` vaut `1` et `False` vaut `0`. Les opérateurs `and` et `or` utilisent l'évaluation en court-circuit et retournent l'un de leurs opérandes. Toute valeur Python a une valeur de vérité (*falsy* ou *truthy*).
- Les **chaînes** (`str`) sont des séquences immuables de caractères Unicode. Les f-strings sont le moyen moderne et recommandé de formater des chaînes. Le slicing offre un accès flexible aux sous-séquences.
- **`None`** est un singleton représentant l'absence de valeur ; on le compare toujours avec `is None`, jamais avec `== None`.
- Python est à **typage dynamique** et favorise le **duck typing** : on s'intéresse aux capacités d'un objet, pas à son type. `isinstance()` est préférable à `type() ==` pour les vérifications de type.
- Les **conversions explicites** (`int()`, `float()`, `str()`, `bool()`) permettent de transformer les valeurs d'un type à l'autre ; les conversions implicites sont limitées et bien définies.

Dans le chapitre suivant, nous aborderons les **structures de contrôle** : conditions, boucles et le filtrage structurel avec `match`/`case`.
