MFormations
Modern Python Engineering

Chapitre 1

01-Fondamentaux-Python

01-Fondamentaux-Python

Fondamentaux Python — Cours Complet

1. Types Fondamentaux

Types numériques

from decimal import Decimal
from fractions import Fraction

# int — précision arbitraire
a: int = 42
b: int = 2**1000  # très grand entier

# float — IEEE 754 double précision
c: float = 3.14159
d: float = 1.5e-10

# Decimal — précision exacte (finances)
e: Decimal = Decimal("0.1") + Decimal("0.2")  # exact: 0.3
f: float = 0.1 + 0.2  # 0.30000000000000004

# Fraction — rationnels exacts
g: Fraction = Fraction(1, 3) + Fraction(1, 6)  # 1/2

Chaînes de caractères

# str — Unicode immuable
s1: str = "hello"
s2: str = "world"
s3: str = f"{s1} {s2}"      # f-string (PEP 498)
s4: str = f"{s1.upper()} {s2!r}"
s5: str = """multi
ligne"""

# Méthodes essentielles
s1.upper(), s1.lower(), s1.title()
s1.strip(), s1.split(), " ".join(["a", "b"])
s1.startswith("h"), s1.endswith("o")
s1.replace("h", "H"), s1.find("e")

# bytes — données binaires
b: bytes = b"hello"
ba: bytearray = bytearray(b"hello")
ba[0] = 72  # mutable

# str → bytes
encoded: bytes = "héllo".encode("utf-8")  # b'h\xc3\xa9llo'
# bytes → str
decoded: str = encoded.decode("utf-8")   # 'héllo'

Listes

# list — séquence mutable ordonnée
lst: list[int] = [1, 2, 3]
lst.append(4)                     # [1, 2, 3, 4]
lst.extend([5, 6])               # [1, 2, 3, 4, 5, 6]
lst.insert(0, 0)                  # [0, 1, 2, 3, 4, 5, 6]
lst.pop()                         # 6
lst.remove(0)                     # [1, 2, 3, 4, 5]
lst.sort(reverse=True)            # [5, 4, 3, 2, 1]

# Slicing
lst = [0, 1, 2, 3, 4, 5]
lst[1:3]      # [1, 2]
lst[:3]       # [0, 1, 2]
lst[3:]       # [3, 4, 5]
lst[::2]      # [0, 2, 4]
lst[::-1]     # [5, 4, 3, 2, 1, 0]

# List as stack — O(1)
stack: list[int] = []
stack.append(1)
stack.append(2)
top = stack.pop()  # 2

# List as queue — O(n) avec pop(0), préférer collections.deque
from collections import deque
queue: deque[int] = deque()
queue.append(1)
queue.append(2)
first = queue.popleft()  # 1 — O(1)

Dictionnaires

# dict — table de hachage (3.7+ : insertion order preserved)
d: dict[str, int] = {"a": 1, "b": 2}
d["c"] = 3                         # ajout
d.get("d", 0)                      # 0 (default)
d.setdefault("e", 5)               # 5 si absent
d.update({"f": 6, "g": 7})

# 3.9+ : merge operators
d1 = {"a": 1, "b": 2}
d2 = {"b": 3, "c": 4}
merged = d1 | d2                    # {"a": 1, "b": 3, "c": 4}
d1 |= d2                            # in-place merge

# Views
for key in d: ...                   # keys (default)
for key, value in d.items(): ...
for value in d.values(): ...

# defaultdict — valeur par défaut automatique
from collections import defaultdict
dd = defaultdict(list)
dd["a"].append(1)                   # pas de KeyError

# Counter — compteur
from collections import Counter
c = Counter("hello world")
c.most_common(3)                    # [('l', 3), ('o', 2), (' ', 1)]

Ensembles

# set — collection non-ordonnée, hachable, unique
s: set[int] = {1, 2, 3, 1}         # {1, 2, 3}
s.add(4)
s.remove(2)                         # KeyError si absent
s.discard(5)                        # safe

# Opérations ensemblistes
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
a | b  # union     -> {1, 2, 3, 4, 5, 6}
a & b  # intersect -> {3, 4}
a - b  # diff      -> {1, 2}
a ^ b  # sym diff  -> {1, 2, 5, 6}

# frozenset — immuable, hachable (clé de dict)
fs: frozenset[int] = frozenset([1, 2, 3])

Tuples

# tuple — séquence immuable, hachable
t: tuple[int, str, float] = (1, "a", 3.14)

# Named tuple
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
p.x, p.y, p[0], p[1]  # accès par nom et index

# Typed named tuple (3.12+)
from typing import NamedTuple
class Employee(NamedTuple):
    name: str
    id: int

e = Employee("Alice", 123)
name, id = e  # unpacking

2. Fonctions

Paramètres avancés

def func(
    a: int,                          # positional
    b: str = "default",              # default
    *args: int,                      # *args — tuple
    c: int,                          # keyword-only (après *)
    d: str = "kw-default",           # keyword-only avec default
    **kwargs: str,                   # **kwargs — dict
) -> None:
    ...

# Appels
func(1, "hello", 2, 3, c=4, d="world", extra="x")

# Positional-only (3.8+)
def divide(a: int, b: int, /) -> float:
    """a et b sont positional-only."""
    return a / b

divide(10, 3)       # OK
# divide(a=10, b=3) # TypeError!

Lambda

# Lambda — fonction anonyme à une expression
square = lambda x: x ** 2
add = lambda a, b: a + b

# Usage typique : sorting
pairs = [(1, "one"), (3, "three"), (2, "two")]
pairs.sort(key=lambda x: x[0])

# map/filter
list(map(lambda x: x * 2, [1, 2, 3]))    # [2, 4, 6]
list(filter(lambda x: x > 0, [-1, 0, 1]))  # [1]

Closures

def make_counter() -> callable:
    """Une closure — une fonction avec un état capturé."""
    count = 0

    def counter() -> int:
        nonlocal count
        count += 1
        return count

    return counter

c1 = make_counter()
c1()  # 1
c1()  # 2
c2 = make_counter()
c2()  # 1 (indépendant)

functools

import functools

# partial — fixe des arguments
def power(base: float, exp: float) -> float:
    return base ** exp

square = functools.partial(power, exp=2)
cube = functools.partial(power, exp=3)
square(5)  # 25
cube(5)    # 125

# reduce — accumulation
from functools import reduce
product = reduce(lambda a, b: a * b, [1, 2, 3, 4])  # 24

# singledispatch — polymorphism par type
from functools import singledispatch

@singledispatch
def process(obj):
    raise NotImplementedError(f"Type {type(obj)} not supported")

@process.register(int)
def process_int(obj: int) -> str:
    return f"Integer: {obj}"

@process.register(str)
def process_str(obj: str) -> str:
    return f"String: {obj}"

@process.register(list)
def process_list(obj: list) -> str:
    return f"List with {len(obj)} items"

process(42)    # "Integer: 42"
process("hi")  # "String: hi"
process([1])   # "List with 1 items"

3. Décorateurs

Principe

Un décorateur est une fonction qui prend une fonction et retourne une fonction modifiée.

Décorateur simple

from collections.abc import Callable
import functools

def timer[**P, T](func: Callable[P, T]) -> Callable[P, T]:
    """Mesure le temps d'exécution."""

    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        import time
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result

    return wrapper

@timer
def compute(n: int) -> int:
    return sum(range(n))

Decorator avec paramètres

def retry[**P, T](max_attempts: int = 3, delay: float = 0.1) -> Callable[[Callable[P, T]], Callable[P, T]]:
    """Réessaie une fonction qui échoue."""

    def decorator(func: Callable[P, T]) -> Callable[P, T]:
        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            import time
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
                    time.sleep(delay)
            raise RuntimeError("Unreachable")  # never reached

        return wrapper

    return decorator

@retry(max_attempts=5, delay=0.5)
def unstable_api_call() -> str:
    import random
    if random.random() < 0.7:
        raise ConnectionError("Network error")
    return "Success"

@lru_cache et @cache

from functools import lru_cache, cache

@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
    """Fibonacci avec mémoïsation."""
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

@cache  # 3.9+ — équivalent à lru_cache(maxsize=None)
def expensive_computation(x: float, y: float) -> float:
    import time
    time.sleep(1)
    return x ** y + y ** x

Decorator en classe

from collections.abc import Callable
import functools

class CountCalls:
    """Compte le nombre d'appels à une fonction."""

    def __init__(self, func: Callable) -> None:
        functools.update_wrapper(self, func)
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)

@CountCalls
def hello(name: str) -> str:
    return f"Hello, {name}!"

hello("Alice")  # "Hello, Alice!"
hello("Bob")    # "Hello, Bob!"
print(hello.count)  # 2

4. Générateurs

yield

def fibonacci(limit: int) -> Generator[int, None, None]:
    """Générateur de la suite de Fibonacci."""
    a, b = 0, 1
    while a < limit:
        yield a
        a, b = b, a + b

for num in fibonacci(100):
    print(num)  # 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89

Generator Expressions

# Generator expression — lazy, mémoire efficace
squares = (x ** 2 for x in range(10_000_000))
first_5 = [next(squares) for _ in range(5)]  # [0, 1, 4, 9, 16]

# vs list comprehension — eager, mémoire
squares_list = [x ** 2 for x in range(10_000_000)]  # ~300MB !

yield from

def chain(*iterables: Iterable[T]) -> Generator[T, None, None]:
    """Chaîne plusieurs itérables en séquence."""
    for iterable in iterables:
        yield from iterable  # délègue à un sous-générateur

# Équivalent sans yield from :
def chain_verbose(*iterables: Iterable[T]) -> Generator[T, None, None]:
    for iterable in iterables:
        for item in iterable:
            yield item

list(chain([1, 2], "ab", (3, 4)))  # [1, 2, 'a', 'b', 3, 4]

send — Bidirectional Generators

def accumulator() -> Generator[int, int, str]:
    """Accumule des valeurs et retourne la somme finale."""
    total = 0
    while True:
        value = yield total  # reçoit une valeur via send()
        if value is None:
            break
        total += value
    return f"Final total: {total}"

gen = accumulator()
next(gen)           # démarre le générateur, retourne 0
gen.send(10)        # 10
gen.send(20)        # 30
gen.send(30)        # 60
try:
    gen.send(None)  # StopIteration avec message
except StopIteration as e:
    print(e.value)  # "Final total: 60"

5. Itérateurs

Le protocole d'itération

class Range:
    """Itérateur personnalisé simulant range()."""

    def __init__(self, start: int, stop: int, step: int = 1) -> None:
        self.current = start
        self.stop = stop
        self.step = step

    def __iter__(self):
        return self  # l'itérateur est son propre itérateur

    def __next__(self) -> int:
        if self.current >= self.stop:
            raise StopIteration
        value = self.current
        self.current += self.step
        return value

# Usage
for i in Range(0, 5):
    print(i)  # 0, 1, 2, 3, 4

# Itérable + itérateur séparés
class MyIterable:
    """Un itérable qui crée un itérateur à chaque itération."""

    def __init__(self, data: list[int]):
        self.data = data

    def __iter__(self):
        return MyIterator(self.data)

class MyIterator:
    def __init__(self, data: list[int]):
        self.data = data
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self) -> int:
        if self.index >= len(self.data):
            raise StopIteration
        value = self.data[self.index]
        self.index += 1
        return value

itertools

import itertools

# Compteurs infinis
counter = itertools.count(start=0, step=2)
next(counter)  # 0, 2, 4, 6, ...
cycle = itertools.cycle("ABC")
next(cycle)    # A, B, C, A, B, ...

# Combinations et permutations
list(itertools.permutations("ABC", 2))
# [('A','B'), ('A','C'), ('B','A'), ('B','C'), ('C','A'), ('C','B')]

list(itertools.combinations("ABC", 2))
# [('A','B'), ('A','C'), ('B','C')]

list(itertools.product("AB", "12"))
# [('A','1'), ('A','2'), ('B','1'), ('B','2')]

# Groupement
data = [("a", 1), ("a", 2), ("b", 3)]
for key, group in itertools.groupby(data, key=lambda x: x[0]):
    print(key, list(group))

# Chaînage et compression
list(itertools.chain([1, 2], [3, 4]))           # [1, 2, 3, 4]
list(itertools.compress("ABCD", [1, 0, 1, 0]))  # ['A', 'C']

# islice — slicing lazy
list(itertools.islice(range(100), 5))  # [0, 1, 2, 3, 4]

# takewhile / dropwhile
list(itertools.takewhile(lambda x: x < 5, [1, 3, 7, 2, 9]))  # [1, 3]
list(itertools.dropwhile(lambda x: x < 5, [1, 3, 7, 2, 9]))  # [7, 2, 9]

# zip_longest
list(itertools.zip_longest("AB", "123", fillvalue="?"))
# [('A', '1'), ('B', '2'), ('?', '3')]

# accumulate — running total
list(itertools.accumulate([1, 2, 3, 4]))  # [1, 3, 6, 10]

6. Context Managers

with statement

# Gestion de ressources
with open("file.txt", "w") as f:
    f.write("hello")

# Multiples contextes
with open("a.txt") as f1, open("b.txt") as f2:
    for line1, line2 in zip(f1, f2):
        print(line1, line2)

# 3.10+ : parenthèses pour multi-lignes
with (
    open("a.txt") as f1,
    open("b.txt") as f2,
):
    pass

Implémentation manuelle

class ManagedFile:
    """Context manager pour fichier."""

    def __init__(self, filename: str, mode: str = "r") -> None:
        self.filename = filename
        self.mode = mode
        self.file = None

    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file:
            self.file.close()
        # Ne pas supprimer l'exception si elle existe
        return False

@contextmanager

from contextlib import contextmanager

@contextmanager
def managed_file(filename: str, mode: str = "r"):
    """Context manager via générateur."""
    file = open(filename, mode)
    try:
        yield file  # point de suspension
    finally:
        file.close()

# Usage
with managed_file("test.txt", "w") as f:
    f.write("hello")

ExitStack

from contextlib import ExitStack

def process_files(filenames: list[str]) -> None:
    """Ouvre un nombre variable de fichiers."""
    with ExitStack() as stack:
        files = [
            stack.enter_context(open(fname))
            for fname in filenames
        ]
        # Tous les fichiers sont fermés automatiquement
        for f in files:
            print(f.read())

# Gestion conditionnelle
def maybe_open(should_open: bool) -> None:
    with ExitStack() as stack:
        if should_open:
            f = stack.enter_context(open("file.txt"))
        # f est fermé seulement si ouvert

Context managers courants

from contextlib import redirect_stdout, redirect_stderr, suppress, nullcontext
import io

# Rediriger stdout
buf = io.StringIO()
with redirect_stdout(buf):
    print("hidden output")
buf.getvalue()  # "hidden output\n"

# Supprimer une exception
with suppress(FileNotFoundError):
    open("nonexistent.txt")

# nullcontext — utile comme placeholder
from contextlib import nullcontext
ctx = nullcontext() if some_condition else managed_file("test.txt")
with ctx as resource:
    ...

7. Comprehensions

List comprehension

# Syntaxe: [expression for item in iterable if condition]

squares = [x ** 2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

evens = [x for x in range(20) if x % 2 == 0]
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

matrix = [[1, 2], [3, 4], [5, 6]]
flattened = [num for row in matrix for num in row]
# [1, 2, 3, 4, 5, 6]

# Nested
pairs = [(x, y) for x in range(3) for y in range(3) if x != y]

Dict comprehension

# Syntaxe: {key: value for item in iterable if condition}

squares_dict = {x: x ** 2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Inverser un dict
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
# {1: 'a', 2: 'b', 3: 'c'}

# Filtrer
filtered = {k: v for k, v in original.items() if v > 1}

Set comprehension

# Syntaxe: {expression for item in iterable if condition}

unique_lengths = {len(word) for word in ["hello", "world", "python"]}
# {5, 6}

even_squares = {x ** 2 for x in range(10) if x % 2 == 0}
# {0, 4, 16, 36, 64}

Perfomance : Comprehension vs Loop

import timeit

# List comprehension (plus rapide)
comp_time = timeit.timeit(
    "[x ** 2 for x in range(1000)]", number=10000
)

# For loop (plus lent)
loop_time = timeit.timeit(
    """
squares = []
for x in range(1000):
    squares.append(x ** 2)
""", number=10000
)

print(f"Comprehension: {comp_time:.3f}s")
print(f"For loop: {loop_time:.3f}s")
# Comprehension ~30-40% plus rapide

8. Tableau Récapitulatif

ConceptSyntaxeUsage
List comp[x for x in items]Transformer/filtrer listes
Dict comp{k: v for k, v in items}Construire dictionnaires
Set comp{x for x in items}Ensemble unique
Gen expr(x for x in items)Itérateur lazy
Lambdalambda x: x + 1Fonction jetable
Decorator@decoratorÉtendre comportement
Generatoryield valueItérateur stateful
Contextwith x as y:Gestion ressources