Canal de preuve, LANXAS AI ONE
Section VII — Programmation, débogage et génie logiciel
Score de la section : 91/100. Questions intégrales, réponses complètes non retouchées, notes et analyses du correcteur indépendant. Chaque réponse est scellée par son empreinte SHA-256, recalculable sur le texte brut en UTF-8.
Q151
3/4H1Horodatage UTC 2026-08-27T00:02:05Z · durée 45.3 s
SHA-256 : 512b2ab7e9cde1e90931649fb32132dbe73ae56347f164ddcf4986c00940c89c
Question
En Python, expliquer le résultat et le risque de `def f(x=[]): x.append(1); return x` lors de plusieurs appels successifs.
Réponse de LANXAS AI ONE
**Résultat observable lors d'appels successifs**
```python
def f(x=[]):
x.append(1)
return x
r1 = f()
r2 = f()
r3 = f()
print(f"1er appel : {r1}")
print(f"2e appel : {r2}")
print(f"3e appel : {r3}")
print(f"Meme objet memoire : {r1 is r2 is r3}")
```
Sortie exacte :
1er appel : [1]
2e appel : [1, 1]
3e appel : [1, 1, 1]
Meme objet memoire : True
**Explication mécanique**
En Python, les arguments par défaut sont évalués une seule fois, au moment de la compilation de la définition de la fonction. `x=[]` crée un seul objet liste en mémoire. Chaque appel sans fournir d'argument réutilise strictement cette même liste. `x.append(1)` modifie l'objet en place, donc l'état persiste d'un appel à l'autre.
**Risques identifiés**
1. Mutation silencieuse d'un état partagé : les appels successifs accumulent des données sans alerte, ce qui rend le comportement non déterministe et difficile à déboguer.
2. Contamination croisée : si la fonction est appelée depuis plusieurs modules ou threads, la liste devient un point de concurrence non protégé, provoquant des courses à données ou des corruptions d'état.
3. Incompatibilité avec les appels partiels : si la fonction est appelée avec un argument fourni (`f([2])`), le comportement change brutalement car un nouvel objet est créé, créant une incohérence sémantique.
4. Fuites mémoire indirectes : la liste grandit indéfiniment tant que la fonction n'est pas rechargée ou que le module n'est pas déchargé, ce qui peut saturer la mémoire dans des boucles ou des workers.
**Correction recommandée**
```python
def f(x=None):
if x is None:
x = []
x.append(1)
return x
```
L'initialisation à `None` garantit qu'un nouvel objet mutable est créé à chaque appel. Aucun état ne traverse les appels, le risque de mutation croisée est supprimé, et l'intention de la fonction redevient prévisible et testable.
Analyse du correcteur : Effet du défaut mutable et correction par None correctement expliqués. L'objet par défaut est créé lors de l'exécution de l'instruction def, non lors de la compilation, et le comportement est déterministe même s'il est indésirable.
Q152
2/4H2Horodatage UTC 2026-08-27T00:03:14Z · durée 68.7 s
SHA-256 : 78361b3b5dece49aac72827e56793382f82563818e9b2b88a8a8f05c09a0e04b
Question
Corriger conceptuellement une fonction qui itère sur une liste tout en supprimant des éléments de cette même liste, en expliquant le bug potentiel.
Réponse de LANXAS AI ONE
### Explication du bug conceptuel
Modifier une collection pendant une itération standard provoque un décalage des indices et une modification de la longueur. Le compteur d'itération progresse de un à chaque tour, mais les éléments suivants glissent vers des indices inférieurs. Conséquence : des éléments sont systématiquement sautés. Dans certains langages, l'itération directe sur une collection modifiée lève une exception (`RuntimeError` ou `ConcurrentModificationException`). Le bug ne vient pas de la condition de suppression, mais de la synchronisation rompue entre le curseur d'itération et la structure mutable.
### Exemple fautif et sortie
```python
liste = [1, 2, 3, 4, 5, 6]
for x in liste:
if x % 2 == 0:
liste.remove(x)
print(liste)
```
**Sortie :** `[1, 3, 5]` (l'élément `6` est sauté car après la suppression de `4`, `6` glisse à l'indice suivant, mais le curseur d'itération a déjà avancé).
### Corrections conceptuelles
Trois approches préservent l'intégrité de l'itération. Chaque méthode est illustrée avec son code et sa sortie exacte.
#### 1. Itération sur une copie superficielle
On parcourt un instantané de la liste tandis que la liste originale est modifiée.
```python
liste = [1, 2, 3, 4, 5, 6]
for x in liste[:]:
if x % 2 == 0:
liste.remove(x)
print(liste)
```
**Sortie :** `[1, 3, 5]`
#### 2. Liste par compréhension (filtrage)
La liste est reconstruite entièrement en une passe sans mutation en cours. C'est la méthode idiomatique en Python.
```python
liste = [1, 2, 3, 4, 5, 6]
liste = [x for x in liste if x % 2 != 0]
print(liste)
```
**Sortie :** `[1, 3, 5]`
#### 3. Itération inverse par indice
En parcourant la liste de la fin vers le début, les suppressions n'affectent jamais les indices non encore visités.
```python
liste = [1, 2, 3, 4, 5, 6]
for i in range(len(liste) - 1, -1, -1):
if liste[i] % 2 == 0:
del liste[i]
print(liste)
```
**Sortie :** `[1, 3, 5]`
### Analyse comparative
| Approche | Complexité temporelle | Complexité spatiale | Risque résiduel |
|---|---|---|---|
| Boucle directe modifiée | O(n) | O(1) | Sauts d'éléments, exceptions |
| Copie `liste[:]` | O(n) | O(n) | Mémoire supplémentaire, non applicable aux très grands flux |
| Compréhension | O(n) | O(n) | Crée un nouvel objet, idéal pour le filtrage pur |
| Itération inverse | O(n) | O(1) | Nécessite un accès indexé, adapté aux suppressions conditionnelles complexes |
### Recommandation
Privilégiez la liste par compréhension pour un filtrage simple. Si la suppression dépend d'effets secondaires ou de conditions complexes nécessitant l'index, utilisez l'itération inverse. Évitez toujours la modification directe de la collection courante pendant son itération.
Analyse du correcteur : Le risque et les trois corrections sont pertinents. Toutefois l'exemple donné ne saute pas 6 : il saute 3 puis 5, sans conséquence sur ce filtre, et produit bien [1,3,5]. En outre, des suppressions répétées par indice dans une liste Python coûtent O(n^2) au pire, pas O(n).
Q153
4/4H0Horodatage UTC 2026-08-27T00:03:57Z · durée 43.0 s
SHA-256 : 2da442b47406f2f167b16f97618eea8fcbeb615d3987a6cbd1ccd4bec20f7745
Question
En Python, quelle différence existe entre `is` et `==` ? Donner un cas où les confondre crée un bug.
Réponse de LANXAS AI ONE
En Python, `==` teste l'égalité des valeurs (il compare le contenu en appelant la méthode `__eq__`), tandis que `is` teste l'identité des objets (il vérifie si deux variables désignent le même emplacement mémoire, c'est-à-dire le même objet unique).
**Cas concret de bug**
Un développeur écrit une fonction de validation de réponses qui utilise `is` pour comparer des listes de choix :
```python
def valider_reponse(utilisateur, attendu):
if utilisateur is attendu:
return "Correct"
return "Incorrect"
choix_eleve = ["A", "B"]
choix_officiel = ["A", "B"]
print(valider_reponse(choix_eleve, choix_officiel))
```
Sortie : `Incorrect`
**Analyse du bug**
`choix_eleve` et `choix_officiel` contiennent exactement les mêmes valeurs. `==` retournerait `True` car il compare le contenu. En revanche, `is` compare l'identité mémoire. Chaque création de liste alloue un nouvel objet en mémoire, donc `id(choix_eleve) != id(choix_officiel)`. Le test `is` échoue systématiquement, ce qui fait rejeter à tort une réponse parfaitement valide. Ce type d'erreur peut corrompre des caches, des validations de types, ou des tests unitaires.
**Règles d'usage**
- Utiliser `==` pour comparer les valeurs (chaînes, nombres, listes, dictionnaires, objets personnalisés). C'est le cas de figure standard.
- Utiliser `is` uniquement pour tester l'identité, notamment avec le singleton `None` (`if x is None:`), ou pour vérifier qu'une variable pointe vers un objet unique en mémoire (ex. `sys.stdout`, singletons métier).
- Ne jamais employer `is` pour comparer des types immuables (entiers, chaînes, tuples) à des fins de validation, car leur internage et leur réutilisation en mémoire dépendent de l'implémentation CPython et ne sont pas garantis par le langage.
Analyse du correcteur : Égalité de valeur et identité correctement distinguées avec un bon exemple.
Q154
4/4H0Horodatage UTC 2026-08-27T00:04:34Z · durée 37.3 s
SHA-256 : 0339894b498e0287347cea02faafcb40832713544b2c629480d87702b2d68b51
Question
Écrire une requête SQL qui retourne, pour chaque client, le montant total de ses commandes, y compris les clients sans commande.
Réponse de LANXAS AI ONE
Comme aucune structure de base de données n'a été fournie, voici la requête basée sur un schéma relationnel standard (table `clients` et table `commandes`).
**Hypothèses de schéma :**
* Table `clients` : colonnes `id_client`, `nom_client`
* Table `commandes` : colonnes `id_client`, `montant`
```sql
SELECT
c.id_client,
c.nom_client,
COALESCE(SUM(cmd.montant), 0) AS total_commandes
FROM clients c
LEFT JOIN commandes cmd ON c.id_client = cmd.id_client
GROUP BY c.id_client, c.nom_client;
```
**Analyse technique :**
1. **`LEFT JOIN`** : C'est l'opérateur indispensable ici. Il conserve toutes les lignes de la table de gauche (`clients`), même lorsqu'il n'existe aucune ligne correspondante dans la table de droite (`commandes`). Un `INNER JOIN` aurait exclu les clients sans commandes.
2. **`SUM(cmd.montant)`** : Calcule l'agrégation des montants pour chaque groupe de client.
3. **`COALESCE(..., 0)`** : Pour un client sans commande, la fonction `SUM` retourne `NULL`. `COALESCE` permet de remplacer ce `NULL` par `0` pour une restitution plus propre des données.
4. **`GROUP BY`** : Regroupe les résultats par client pour appliquer la fonction d'agrégation `SUM` individuellement à chacun.
Analyse du correcteur : LEFT JOIN, SUM, COALESCE et GROUP BY correctement utilisés.
Q155
4/4H0Horodatage UTC 2026-08-27T00:05:10Z · durée 35.9 s
SHA-256 : 61fa270e06c78d46e6336ff943ed0986e667f0aa187add94f77c9bffbe969268
Question
Détecter le bug dans ce pseudo-code de recherche binaire : `while lo < hi: mid=(lo+hi)//2; if a[mid]<x: lo=mid; else: hi=mid`.
Réponse de LANXAS AI ONE
Le bug principal est une **boucle infinie** provoquée par la mise à jour `lo = mid`.
**Mécanisme de l'erreur**
Lorsque les indices `lo` et `hi` sont adjacents (par exemple `lo = 3`, `hi = 4`), la division entière donne `mid = (3 + 4) // 2 = 3`. Si la condition `a[mid] < x` est vraie, l'algorithme exécute `lo = mid`, ce qui conserve `lo = 3`. Les bornes `lo` et `hi` ne changent plus, la condition `lo < hi` reste strictement vraie, et `mid` sera recalculé indéfiniment à 3. L'algorithme stagne.
**Correction immédiate**
Il faut toujours avancer d'au moins une position pour garantir la progression vers la termination :
```
while lo < hi:
mid = (lo + hi) // 2
if a[mid] < x:
lo = mid + 1
else:
hi = mid
```
Avec cette correction, le cas `lo = 3`, `hi = 4` évolue correctement : `mid = 3`, si `a[3] < x` alors `lo = 4`, la boucle s'arrête immédiatement.
**Remarques structurelles**
1. L'algorithme tel qu'écrit ne retourne aucune valeur. Il est conçu pour trouver la première occurrence de `x` ou le point d'insertion, auquel cas il faut retourner `lo` (ou `hi`, qui sont égaux) après la boucle.
2. Si l'objectif est de vérifier strictement l'existence de `x`, il faut ajouter un test post-boucle `if lo < len(a) and a[lo] == x: return lo` pour éviter un faux positif lors d'une recherche inexistante.
3. La borne supérieure `hi = mid` est correcte dans ce paradigme (recherche de première occurrence/insertion), car `hi` représente un candidat valide. La borne inférieure `lo` représente une zone strictement inférieure à `x`, d'où le `+ 1`.
Analyse du correcteur : Boucle infinie correctement diagnostiquée et correction lo=mid+1 exacte.
Q156
4/4H0Horodatage UTC 2026-08-27T00:05:42Z · durée 32.1 s
SHA-256 : 772a361c51d90a38c02e228098889bd5435795118a47e3814d66a3714aa12533
Question
Expliquer ce qu’est une condition de course et donner une stratégie de prévention.
Réponse de LANXAS AI ONE
Une condition de course est un dysfonctionnement logiciel qui survient lorsque deux ou plusieurs processus ou threads accèdent simultanément à une ressource partagée (variable, fichier, base de données, périphérique) et que l’un d’eux modifie l’état de cette ressource sans mécanisme de synchronisation. Le résultat final dépend alors de l’ordre d’exécution imprévisible des opérations, ce qui rend le comportement du système non déterministe.
Mécanisme : le problème repose sur trois conditions simultanées : (1) accès concurrent, (2) état mutable partagé, (3) absence de verrouillage ou d’atomicité sur la section critique. Par exemple, si deux threads incrémentent une variable `compteur` initialement à 0, l’opération `compteur += 1` se décompose en lecture-modification-écriture. Si les deux threads lisent 0 simultanément, écrivent chacun 1, le résultat final sera 1 au lieu de 2.
Stratégies de prévention :
- Synchronisation explicite : utiliser des mutex, des sémaphores ou des moniteurs pour protéger les sections critiques. Un mutex garantit qu’un seul thread exécute la section critique à la fois.
- Opérations atomiques : exploiter les instructions matérielles (CAS, fetch-and-add) ou les types atomiques des langages modernes pour garantir l’exécution indivisible des opérations sur des données partagées.
- Immuabilité et isolation : concevoir les données comme immuables après création, ou utiliser un stockage local au thread (thread-local storage) pour éliminer le partage.
- Modèles de concurrence sans état partagé : privilégier la transmission de messages (modèle acteur, canaux), la mémoire transactionnelle logicielle (STM) ou les systèmes réactifs où chaque composant possède son propre état isolé.
- Programmation sans verrou (lock-free/wait-free) : lorsque la performance l’exige, utiliser des structures de données concurrentes validées (queues, stacks, hash tables) basées sur des opérations atomiques et des algorithmes vérifiés formellement.
Vérification et bonnes pratiques :
- Analyser statiquement le code avec des outils de détection de race (ThreadSanitizer, Helgrind, Coverity) et tester sous charge élevée pour révéler les timings critiques.
- Appliquer le principe du moindre privilège applicatif : limiter l’accès aux ressources partagées au strict nécessaire et séparer clairement les phases de migration et d’exécution.
- Documenter les invariants de synchronisation et éviter les verrous imbriqués non ordonnés (prévenir les interblocages tout en maintenant la protection).
- Privilégier les bibliothèques standard thread-safe plutôt que d’implémenter des mécanismes maison non vérifiés.
Une condition de course ne peut être éliminée que par la suppression du partage mutable non synchronisé ou par une synchronisation vérifiée. Aucune stratégie ne garantit l’absence totale de risque sans validation continue (tests de stress, vérification formelle, surveillance en production).
Analyse du correcteur : Condition de course correctement définie et stratégies de prévention adaptées.
Q157
4/4H0Horodatage UTC 2026-08-27T00:06:39Z · durée 56.4 s
SHA-256 : 584bfa4deaf5886d62050bd05c03c69922fc60942c0db16d80856ca09d7fafce
Question
Une API REST répond 500 pour une entrée utilisateur invalide. Quel type de statut HTTP serait plus approprié et pourquoi ?
Réponse de LANXAS AI ONE
Le statut HTTP approprié serait le 400 Bad Request, ou le 422 Unprocessable Entity selon le contexte de validation.
Pourquoi le 500 est inadapté :
Le code 500 (Internal Server Error) signale une erreur inattendue côté serveur (plantage, exception non gérée, ressource indisponible). L’utiliser pour une entrée invalide induit le client en erreur : il interprète la réponse comme un dysfonctionnement serveur et peut déclencher des tentatives de renvoi (retry) inappropriées ou masquer un problème de validation côté client.
Pourquoi le 400 ou le 422 est correct :
Le code 400 indique que la requête est malformée syntaxiquement ou contient des paramètres invalides (type de donnée incorrect, valeur hors bornes, syntaxe JSON ou XML cassée). C’est le code standard HTTP/1.1 (RFC 7231) pour les erreurs d’entrée client.
Le code 422 est plus précis si la syntaxe de la requête est valide mais que la sémantique échoue à la validation métier (champ requis manquant, email invalide, contraintes uniques violées). Il est largement adopté dans les APIs REST modernes.
Différenciation pratique :
Entrée syntaxiquement invalide : 400 Bad Request. La requête ne respecte pas le format attendu.
Entrée syntaxiquement valide mais sémantiquement invalide : 422 Unprocessable Entity. La structure est correcte, mais les données violent les règles métier.
Conformité REST :
La classification HTTP réserve les 4xx aux erreurs client et les 5xx aux erreurs serveur. Retourner un 4xx garantit que le client peut différencier un bug serveur d’un problème d’entrée, appliquer une logique de correction adaptée, et respecter les bonnes pratiques de résilience et de débogage. Utiliser un 500 pour une erreur client est une violation sémantique des normes HTTP.
Analyse du correcteur : Codes 400 et 422 correctement distingués d'une erreur serveur 500.
Q158
4/4H0Horodatage UTC 2026-08-27T00:07:12Z · durée 32.9 s
SHA-256 : befc90671d5b8d85d83c736ffb2e059cc5a21715b8ef111a405dd07817d479a8
Question
Expliquer la différence entre authentification et autorisation avec un exemple précis.
Réponse de LANXAS AI ONE
**Authentification**
Processus de vérification de l'identité d'une entité. Elle répond à la question « Qui êtes-vous ? ». Elle repose sur un ou plusieurs facteurs de preuve : quelque chose que vous connaissez (mot de passe, code PIN), quelque chose que vous possédez (téléphone, clé matérielle, certificat) ou quelque chose qui vous caractérise (empreinte digitale, reconnaissance faciale, iris). Le résultat est une identité établie et, généralement, un jeton ou une session.
**Autorisation**
Processus de détermination des droits d'accès et des actions permises à une identité déjà vérifiée. Elle répond à la question « Que pouvez-vous faire ou consulter ? ». Elle s'appuie sur des politiques de sécurité, des rôles (RBAC), des attributs (ABAC), des listes de contrôle d'accès (ACL) ou des portées de jetons (scopes). Le résultat est une décision d'accès (autoriser, refuser, limiter) pour une ressource ou une opération spécifique.
**Différence fondamentale**
L'authentification établit l'identité (preuve de qui l'on est) et précède toujours l'autorisation. L'autorisation définit les permissions associées à cette identité (ce qui est autorisé). Une authentification réussie ne garantit en aucun cas l'accès à une ressource sensible sans l'autorisation correspondante.
**Exemple précis : accès à une plateforme de gestion de paie en entreprise**
1. **Authentification** : L'employé ouvre l'application, saisit son identifiant et son mot de passe, puis valide un code reçu par SMS. Le serveur vérifie ces informations, confirme qu'il s'agit bien de l'employé Jean Dupont et lui délivre un jeton d'accès.
2. **Autorisation** : Jean Dupont tente d'ouvrir la page « Bulletin de paie de Marie Leroy ». Le moteur d'autorisation analyse son jeton, identifie son rôle « employé » et la politique d'accès « un utilisateur ne peut consulter que ses propres données ». La ressource appartient à une autre identité, donc l'autorisation est refusée. Le serveur renvoie une erreur 403 Forbidden.
3. **Autorisation avancée** : Un membre des RH tente de modifier le salaire de Jean Dupont. Le système vérifie que l'utilisateur possède le rôle « responsable RH », que la ressource cible est bien le dossier de paie de Jean Dupont, et que l'heure de connexion est dans la plage autorisée pour les modifications financières. Si ces conditions sont remplies, l'autorisation est accordée et l'action aboutit. Si le RH tente de supprimer définitivement le dossier, le système vérifie une permission supplémentaire « suppression_critique » non attribuée à ce rôle, et bloque l'opération.
Ce mécanisme garantit que seul un accès vérifié est possible, et que chaque action reste strictement contenue dans les permissions attribuées.
Analyse du correcteur : Authentification et autorisation clairement distinguées avec exemple précis.
Q159
4/4H0Horodatage UTC 2026-08-27T00:07:36Z · durée 24.2 s
SHA-256 : e5424b8e2b5de9ee56be98e57011ad27189696330c3c3c98cdb55e92540a3c5e
Question
Proposer des tests unitaires minimaux pour une fonction `divide(a,b)` qui doit refuser la division par zéro.
Réponse de LANXAS AI ONE
Voici une suite minimale de tests unitaires en Python, utilisant le framework standard `unittest` :
```python
import unittest
def divide(a, b):
if b == 0:
raise ZeroDivisionError("Division par zéro impossible")
return a / b
class TestDivide(unittest.TestCase):
def test_division_normale(self):
self.assertAlmostEqual(divide(10, 2), 5.0)
def test_division_par_zero(self):
with self.assertRaises(ZeroDivisionError):
divide(4, 0)
if __name__ == '__main__':
unittest.main()
```
Explications :
- `test_division_normale` : vérifie le résultat attendu pour un appel valide. `assertAlmostEqual` gère les flottants sans problème d'arrondi.
- `test_division_par_zero` : utilise `assertRaises` pour capturer l'exception attendue. Le test passe si l'exception est levée, échoue sinon, et n'interrompt pas l'exécution des autres cas.
- La fonction cible lève explicitement `ZeroDivisionError`, ce qui correspond au comportement standard de Python et à la demande de refus clair.
- Cette suite reste strictement minimale. En environnement de production, on ajouterait des vérifications de type, des tests sur les cas limites (négatifs, zéro au numérateur) et une assertion sur le message d'erreur si la sémantique métier l'exige.
Analyse du correcteur : Jeu minimal couvrant cas nominal et division par zéro correct.
Q160
4/4H0Horodatage UTC 2026-08-27T00:08:04Z · durée 28.7 s
SHA-256 : e4ecb0f31392e2cc84206655e151b820e2c7c3a6595c7f6bb2fea04f803dda9f
Question
En SQL, expliquer la différence entre `WHERE` et `HAVING`.
Réponse de LANXAS AI ONE
En SQL, `WHERE` et `HAVING` servent tous deux à filtrer des données, mais ils opèrent à des étapes différentes du traitement de la requête et à des niveaux distincts.
**1. Moment d'exécution et niveau de filtrage**
- `WHERE` filtre les lignes *individuelles* avant tout regroupement (GROUP BY) ou calcul d'agrégat. Il applique une condition à chaque ligne brute issue de la table.
- `HAVING` filtre les *groupes* générés par un GROUP BY. Il applique une condition sur les résultats agrégés.
**2. Utilisation des fonctions d'agrégation**
- `WHERE` ne peut pas contenir de fonctions d'agrégation (SUM, COUNT, AVG, MAX, MIN, etc.). Si vous tentez de l'utiliser, le moteur SQL renverra une erreur de syntaxe ou sémantique.
- `HAVING` est spécifiquement conçu pour utiliser des fonctions d'agrégation dans ses conditions.
**3. Interaction avec GROUP BY**
- `WHERE` s'exécute avant le GROUP BY. Il réduit le nombre de lignes qui entreront dans le regroupement.
- `HAVING` s'exécute après le GROUP BY. Il réduit le nombre de groupes qui seront retournés.
**4. Ordre logique d'exécution**
Dans une requête SELECT, l'ordre logique (différent de l'ordre d'écriture) est le suivant :
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
Cela confirme que WHERE agit en amont du regroupement, tandis que HAVING agit en aval.
**5. Exemple concret**
```sql
SELECT client_id, SUM(montant) as total_achat
FROM ventes
WHERE date_vente >= '2023-01-01'
GROUP BY client_id
HAVING SUM(montant) > 1000;
```
- `WHERE date_vente >= '2023-01-01'` élimine les lignes dont la date est antérieure à 2023 avant le calcul des totaux.
- `HAVING SUM(montant) > 1000` ne conserve que les groupes (clients) dont le total agrégé dépasse 1000.
**Résumé pratique**
- Utilisez `WHERE` pour filtrer des conditions sur les données brutes (colonnes simples, dates, identifiants, chaînes de caractères).
- Utilisez `HAVING` pour filtrer des conditions sur des calculs agrégés après un regroupement.
- Les deux peuvent coexister dans une même requête ; `WHERE` réduit d'abord le volume de données, puis le regroupement s'effectue, et enfin `HAVING` affine le résultat groupé.
Analyse du correcteur : WHERE avant agrégation et HAVING après regroupement correctement expliqués.
Q161
3/4H1Horodatage UTC 2026-08-27T00:09:04Z · durée 59.8 s
SHA-256 : 9bd238c202599fbb967922cb7fbf7e8633ce34d45f0ad411bc9491da189b38c5
Question
Expliquer pourquoi stocker des mots de passe en clair est incorrect et décrire la stratégie de stockage appropriée.
Réponse de LANXAS AI ONE
Stockérer des mots de passe en clair est une pratique sévèrement interdite par les standards de sécurité et la réglementation. Voici pourquoi cette approche est incorrecte, puis la stratégie appropriée à mettre en œuvre.
Pourquoi le stockage en clair est incorrect :
- Exposition totale en cas de compromission : si la base de données est volée ou si un accès non autorisé est obtenu, les attaquants disposent immédiatement de tous les mots de passe sans aucune étape de décryptage.
- Amplification des impacts : les utilisateurs réutilisent souvent le même mot de passe sur plusieurs services. La fuite expose donc non seulement votre application, mais aussi les comptes externes des utilisateurs.
- Menaces internes : tout personnel autorisé à lire la base de données peut voir les mots de passe, ce qui viole le principe du moindre privilège et crée un risque de détournement ou de chantage.
- Non-conformité légale : le RGPD et les normes sectorielles (PCI-DSS, ISO 27001, NIS 2) exigent la protection des données sensibles. Un stockage en clair constitue une violation directe, exposant l'organisation à des sanctions et à une perte de confiance.
- Impossibilité de récupération sécurisée : en cas d'oubli, le seul recours est une réinitialisation par e-mail ou SMS, ce qui transfère la charge de sécurité vers des canaux souvent moins protégés.
Stratégie de stockage appropriée :
- Hachage unidirectionnel : utiliser une fonction de hachage cryptographique dédiée aux mots de passe, conçue pour être lente et résistante aux attaques par force brute. Les algorithmes recommandés sont Argon2id, bcrypt ou scrypt. Les fonctions rapides comme SHA-256 ou MD5 sont strictement interdites pour cet usage.
- Sel unique par mot de passe : générer un sel cryptographiquement aléatoire (au moins 16 octets) pour chaque utilisateur et l'associer au mot de passe avant le hachage. Cela empêche les attaques par tables arc-en-ciel et garantit que deux mots de passe identiques produisent des hash distincts.
- Paramètres adaptatifs : configurer la complexité computationnelle (coût, itérations, mémoire) selon la puissance matérielle disponible, de manière à ce que le hachage prenne quelques centaines de millisecondes. Ces paramètres doivent être régulièrement augmentés pour suivre l'évolution du matériel.
- Stockage sécurisé des valeurs : ne conserver que le hash et le sel (ou la chaîne complète formatée selon la bibliothèque utilisée). Ne jamais stocker le mot de passe en clair, ni en base64, ni chiffré avec une clé symétrique réversible, car le base64 ou l'hexadécimal ne sont pas du chiffrement.
- Gestion des accès et de l'infrastructure : séparer strictement les bases de données utilisateur des autres systèmes, appliquer le moindre privilège aux rôles applicatifs, chiffrer les données au repos et en transit, et éviter toute journalisation des mots de passe ou de leurs dérivés.
- Procédures de migration et de renouvellement : prévoir un mécanisme de rehashing transparent lors de la connexion (si l'algorithme ou les paramètres sont obsolètes) et prévoir des audits réguliers pour détecter les mauvaises pratiques de stockage.
Cette approche garantit que même en cas de compromission totale de la base de données, les attaquants ne peuvent pas retrouver les mots de passe originaux, tout en respectant les exigences de confidentialité et de conformité.
Analyse du correcteur : Stratégie Argon2id/bcrypt/scrypt avec sel et coût adaptatif correcte. Les affirmations de violation juridique directe et d'impossibilité de retrouver les mots de passe après compromission sont trop absolues : le RGPD impose des mesures appropriées et les mots de passe faibles restent attaquables hors ligne.
Q162
4/4H0Horodatage UTC 2026-08-27T00:09:29Z · durée 24.3 s
SHA-256 : b85a27849760d3f57cddaabd9bb82d0e481d6a2a0e3bf17899a49fa61ce41c97
Question
Donner la sortie de ce code Python : `a=[1,2,3]; b=a; b.append(4); print(a)` et expliquer.
Réponse de LANXAS AI ONE
Sortie :
`[1, 2, 3, 4]`
Explication :
- `a=[1,2,3]` crée un objet liste en mémoire et affecte sa référence au nom `a`.
- `b=a` ne duplique pas la liste. Il affecte simplement la même référence d'objet au nom `b`. `a` et `b` pointent donc vers le même emplacement mémoire.
- `b.append(4)` modifie l'objet liste en place via la référence `b`. Puisque `a` partage cette référence, la modification est immédiatement visible depuis `a`.
- `print(a)` affiche la liste modifiée, soit `[1, 2, 3, 4]`.
Pour créer une liste indépendante, il faut effectuer une copie explicite (par exemple `b = a.copy()` ou `b = a[:]`), ce qui allouerait un nouvel objet en mémoire.
Analyse du correcteur : Alias de liste et sortie [1,2,3,4] correctement expliqués.
Q163
4/4H0Horodatage UTC 2026-08-27T00:10:00Z · durée 31.5 s
SHA-256 : 2c565621eeeba54762c703065a03ba1b293c61851905bfda50d99f658081deda
Question
Identifier le problème de ce code : `try: risky(); except: pass` et proposer une meilleure approche.
Réponse de LANXAS AI ONE
**Problème identifié**
La clause `except: pass` intercepte *toutes* les exceptions, y compris `BaseException` (qui inclut `KeyboardInterrupt`, `SystemExit`, `GeneratorExit`). Le mot-clé `pass` avale silencieusement l'erreur sans journalisation, sans traitement ni propagation.
**Risques concrets**
- Masquage total des dysfonctionnements : aucun traceback, aucun log, aucun mécanisme de fallback.
- État incohérent possible : la fonction `risky()` peut avoir partiellement exécuté du code avec effets de bord, puis le programme continue sans en être informé.
- Violation du principe de moindre privilège : capturer `BaseException` empêche l'utilisateur d'interrompre proprement le processus (`Ctrl+C`) et peut bloquer les mécanismes internes de l'interpréteur.
- Maintenance impossible : la pile d'appels est perdue, rendant le débogage en production quasi impossible.
**Approche recommandée**
```python
import logging
logger = logging.getLogger(__name__)
try:
risky()
except SpecificError as e:
logger.error("Erreur spécifique détectée : %s", e)
# Gestion adaptée : valeur par défaut, réessai limité, fallback, etc.
except Exception as e:
logger.exception("Erreur inattendue : %s", e)
raise # Propager si l'état du programme ne permet pas de continuer proprement
finally:
# Nettoyage des ressources (fichiers, connexions, locks) si nécessaire
pass
```
**Justification des améliorations**
- Ciblage précis : `except SpecificError as e` ne capture que les erreurs attendues. Les exceptions non prévues remontent explicitement.
- Journalisation structurée : `logger.exception()` enregistre automatiquement le niveau `ERROR` + la pile d'appels complète, facilitant le diagnostic.
- Séparation des responsabilités : les erreurs gérables sont traitées localement ; les erreurs critiques sont propagées via `raise`.
- Bloc `finally` (ou context manager) : garantit la libération des ressources même en cas d'exception, respectant le principe de fermeture propre.
- Respect des standards Python : PEP 8 et les guidelines de gestion d'erreurs recommandent explicitement d'éviter les clauses `except` nues et le `pass` dans les blocs de capture.
**Bonnes pratiques associées**
- Ne jamais utiliser `except: pass` en production.
- Privilégier les context managers (`with ...`) pour la gestion des ressources.
- Limiter le nombre de réessais avec des backoffs exponentiels si le contexte le permet.
- Configurer un logger avec un niveau et un handler adaptés à l'environnement (fichier, syslog, système de monitoring).
Analyse du correcteur : Capture nue et silencieuse correctement critiquée, alternative ciblée et journalisée correcte.
Q164
4/4H0Horodatage UTC 2026-08-27T00:10:45Z · durée 45.2 s
SHA-256 : c69612e8adcb836ec64a708c1c0329a823f85cdf28c763e84becf392cfe26a6e
Question
Expliquer la différence entre concurrence et parallélisme.
Réponse de LANXAS AI ONE
La concurrence et le parallélisme sont des concepts distincts en informatique, souvent confondus car ils traitent tous deux du traitement simultané de plusieurs tâches, mais ils relèvent de préoccupations fondamentalement différentes.
**Concurrence** désigne la capacité d'un système à composer plusieurs tâches qui progressent en se chevauchant, sans nécessairement s'exécuter en même temps physiquement. Elle repose sur l'intercalation des exécutions. Un processeur unique peut gérer une concurrence élevée via des fils d'exécution, des coroutines ou des boucles d'événements asynchrones. L'objectif principal est la réactivité, la modularité et l'efficacité dans la gestion des opérations bloquantes (entrées-sorties, réseaux).
**Parallélisme** désigne l'exécution simultanée de plusieurs tâches sur plusieurs unités de calcul. Il nécessite un support matériel multiprocesseur ou multicœur. L'objectif principal est l'accélération du traitement en divisant une charge de travail en sous-tâches indépendantes qui s'exécutent réellement au même instant.
**Différence fondamentale**
La concurrence est un problème de structure et d'ordonnancement, tandis que le parallélisme est un problème d'exécution physique et de performance. On peut avoir de la concurrence sans parallélisme (un seul cœur qui alterne entre plusieurs tâches), et du parallélisme sans concurrence (plusieurs cœurs exécutant des parties d'un même algorithme séquentiellement divisé).
**Analogie**
- Concurrence : un cuisinier prépare un plat en alternant entre hacher des légumes et laisser mijoter une sauce. Les deux processus avancent, mais un seul outil est utilisé à la fois.
- Parallélisme : deux cuisiniers préparent deux préparations en même temps sur deux plans de travail distincts.
**Tableau comparatif**
| Critère | Concurrence | Parallélisme |
|---|---|---|
| Objectif | Réactivité, modularité, gestion des I/O | Performance, accélération computationnelle |
| Support matériel | Possible sur un seul cœur | Nécessite plusieurs cœurs ou processeurs |
| Mécanisme | Interruption, changement de contexte, asynchronisme | Multiprocessing, multithreading matériel, SIMD/GPU |
| Déterminisme | Souvent non déterministe (gestion des ressources critiques) | Déterministe si les tâches sont indépendantes |
| Exemple technique | Node.js (event loop), coroutines Python | MPI, OpenMP, calcul scientifique distribué |
En résumé, la concurrence répond à la question comment organiser plusieurs tâches qui se chevauchent dans le temps, tandis que le parallélisme répond à la question comment exécuter plusieurs tâches en même temps pour gagner du temps. Les deux approches sont souvent combinées dans les systèmes modernes pour allier réactivité et performance brute.
Analyse du correcteur : Concurrence et parallélisme correctement distingués.
Q165
3/4H1Horodatage UTC 2026-08-27T00:11:15Z · durée 29.3 s
SHA-256 : 09128b84493646a03bff2a62a6e95ac6e5c71e9cd3a17dc28271f6466c238b67
Question
Décrire un scénario où une transaction de base de données nécessite un niveau d’isolation élevé pour éviter une anomalie.
Réponse de LANXAS AI ONE
**Contexte opérationnel**
Système de réservation de places pour un événement à capacité strictement limitée (stock initial = 1). Deux clients (T1 et T2) lancent une réservation simultanément via des connexions différentes.
**Transactions concurrentes**
- T1 : `SELECT places_disponibles FROM spectacle WHERE id = 101;` → lit 1
- T2 : `SELECT places_disponibles FROM spectacle WHERE id = 101;` → lit 1
- T1 : `UPDATE spectacle SET places_disponibles = places_disponibles - 1 WHERE id = 101;`
- T2 : `UPDATE spectacle SET places_disponibles = places_disponibles - 1 WHERE id = 101;`
**Anomalie sans niveau élevé**
Avec un niveau `READ COMMITTED` ou `READ UNCOMMITTED`, T2 peut lire la valeur 1 avant que T1 n'ait validé son `UPDATE`. Les deux transactions décrémentent le stock à partir de 1, produisant un résultat final de -1. Il s'agit d'une anomalie de **mise à jour perdue** (ou de **lecture non répétable** selon l'implémentation), violant l'intégrité métier et entraînant une survente.
**Niveau d'isolation requis**
`SÉRIALISABLE` (le niveau le plus élevé standard SQL).
**Mécanisme et vérification**
- Le SGBD applique des verrous de plage (`range locks`) ou un protocole MVCC strict sur les lignes et les plages consultées.
- Toute tentative de lecture ou modification concurrente sur les données concernées est bloquée ou transformée en exécution séquentielle.
- Résultat : l'une des deux transactions attend que l'autre valide ou échoue. Si T1 valide en premier, T2 relit les données ; le `SELECT` retourne 0, et l'`UPDATE` échoue ou est annulé, préservant la cohérence.
- Ce niveau élimine les trois anomalies classiques : lecture sale, lecture non répétable et lecture fantôme.
**Note technique**
En pratique, les développeurs privilégient souvent `REPEATABLE READ` couplé à `SELECT ... FOR UPDATE` (verrouillage pessimiste explicite) pour limiter la contention tout en évitant la mise à jour perdue. Toutefois, si la consigne impose un niveau d'isolation natif élevé sans ajout de syntaxe de verrouillage, `SÉRIALISABLE` est la seule garantie standard de cohérence absolue sur ce scénario.
Analyse du correcteur : Scénario de survente et recours à SERIALIZABLE ou SELECT FOR UPDATE pertinents. Le résultat -1 issu de deux décréments n'est toutefois pas une mise à jour perdue ; c'est un défaut de contrôle atomique de l'invariant métier.
Q166
4/4H0Horodatage UTC 2026-08-27T00:11:45Z · durée 30.6 s
SHA-256 : 90ec3fae6ddc141d73d1c01094c3117b37ecccb569b152b2e2506675bf67f1f4
Question
Écrire une fonction Python conceptuelle qui teste si une chaîne est un palindrome en O(n) temps et O(1) mémoire auxiliaire.
Réponse de LANXAS AI ONE
Voici la fonction conceptuelle respectant strictement les contraintes de complexité $O(n)$ en temps et $O(1)$ en mémoire auxiliaire :
```python
def est_palindrome(chaine):
gauche = 0
droite = len(chaine) - 1
while gauche < droite:
if chaine[gauche] != chaine[droite]:
return False
gauche += 1
droite -= 1
return True
```
**Vérification des complexités :**
1. **Complexité temporelle $O(n)$** :
* La fonction utilise deux pointeurs (`gauche` et `droite`) qui convergent vers le centre de la chaîne.
* La boucle `while` s'exécute au maximum $n/2$ fois (où $n$ est la longueur de la chaîne).
* L'accès par index (`chaine[gauche]`) est une opération en $O(1)$ en Python.
* Le nombre total d'opérations est proportionnel à $n$, soit $O(n)$.
2. **Complexité spatiale $O(1)$** :
* La fonction ne crée aucune nouvelle structure de données (comme une sous-chaîne ou une liste).
* Elle n'alloue que deux variables entières (`gauche` et `droite`) dont la taille mémoire est fixe, indépendamment de la taille de l'entrée.
* L'espace auxiliaire est constant.
**Note technique :**
L'approche naïve utilisant le tranchage (`chaine == chaine[::-1]`) est rejetée ici car l'inversion de chaîne en Python crée une nouvelle copie en mémoire, ce qui porterait la complexité spatiale à $O(n)$.
Analyse du correcteur : Algorithme à deux indices correct en O(n) temps et O(1) espace.
Q167
4/4H0Horodatage UTC 2026-08-27T00:11:58Z · durée 13.3 s
SHA-256 : 1703da5ed5d89bfe65b86d2e4ee54ea1f173463fea4bf4f92a9ec7dc56652b57
Question
Expliquer l’intérêt d’un système de types statique et citer au moins une catégorie d’erreurs qu’il peut détecter avant exécution.
Réponse de LANXAS AI ONE
Un système de types statique vérifie la conformité des types à la compilation, avant toute exécution. Son intérêt principal est de capturer les incohérences structurelles du programme en amont, ce qui augmente la fiabilité globale, réduit le nombre de bugs en production et permet au compilateur d’appliquer des optimisations plus agressives. Il améliore également l’expérience développeur en activant l’autocomplétion précise, la navigation dans le code, la refactorisation sûre et une forme de documentation intégrée.
Catégorie d’erreurs détectée avant exécution : l’incompatibilité de types. Par exemple, le compilateur refusera l’affectation d’une chaîne de caractères à une variable déclarée comme un entier, l’ajout d’une fonction à un tableau d’entiers, ou l’appel d’une méthode qui n’appartient pas au type déclaré.
Analyse du correcteur : Intérêt du typage statique et catégorie d'erreurs correctement donnés.
Q168
3/4H1Horodatage UTC 2026-08-27T00:13:06Z · durée 67.2 s
SHA-256 : a8d332983220c05b69f2876baeeeeb02ff65cc8cab1cb1a7279d18e5456947c4
Question
Un service appelle trois dépendances réseau séquentiellement. Donner deux stratégies pour réduire la latence globale sans compromettre la correction.
Réponse de LANXAS AI ONE
Stratégie 1 : Exécution concurrente des appels indépendants
Principe : Si les trois dépendances n’ont aucune dépendance fonctionnelle (aucune sortie n’est nécessaire en entrée d’une autre), il est possible de les invoquer simultanément. La latence globale passe de la somme des latences individuelles à la valeur maximale de ces dernières, soit Latence_totale ≈ max(L1, L2, L3) au lieu de L1 + L2 + L3.
Garantie de correction : Vérifier l’absence de dépendance de données ou d’état partagé pendant l’exécution. Utiliser un graphe de dépendance fonctionnel ou des tests de régression pour confirmer que l’ordre d’exécution n’impacte pas le résultat final. Agréger les réponses de manière atomique ou utiliser des variables immuables pour éviter les conditions de course. S’assurer que les appels sont idempotents ou que les échecs partiels sont gérés par des mécanismes de compensation sans effet de bord.
Mise en œuvre : Remplacer l’enchaînement synchrone par un appel concurrentiel natif au runtime (asyncio.gather, Promise.all, CompletableFuture.allOf, ou pool de threads). Configurer des timeouts individuels et des politiques de retry adaptées. Mesurer le gain via des traces distribuées pour valider que la latence maximale remplace bien la somme.
Stratégie 2 : Réutilisation des connexions et optimisation de la pile réseau
Principe : Les appels séquentiels créent souvent des surcoûts répétés liés à la résolution DNS, à l’établissement TCP et à la négociation TLS. Maintenir des connexions persistantes et réutiliser un pool évite ces phases d’initialisation à chaque requête. Le passage à HTTP/2 permet en outre le multiplexage de plusieurs flux sur une seule connexion TCP, ce qui réduit la contention et la latence de contrôle.
Garantie de correction : S’assurer que les serveurs distants acceptent les connexions persistantes et le multiplexage. Vérifier que la mise en cache des résultats DNS ou TLS ne viole pas les politiques de sécurité ou les délais de renouvellement des certificats. Les en-têtes de contrôle (Connection: keep-alive, HTTP/2) sont transparents pour la logique métier et ne modifient pas le contenu des échanges applicatifs.
Mise en œuvre : Configurer un client réseau avec un pool de connexions réutilisable (keep-alive activé, max_conn par hôte ajusté), activer le support HTTP/2, mettre en cache les résolutions DNS avec un TTL court, et pré-négocier les sessions TLS si le protocole le permet. Ces optimisations réduisent la latence de 20 à 50 % par appel sans modifier le flux de données applicatif. Valider le comportement via des tests de charge et des mesures de temps de mainlevée TCP/TLS.
Vérification croisée : Pour chaque stratégie, comparer les métriques avant et après (latence réseau, temps CPU, taux d’erreur). S’assurer que les SLA de correction (cohérence des données, gestion des timeouts, isolation des pannes) restent strictement respectés. Adapter le choix en fonction de la nature des dépendances (indépendantes vs fonctionnellement liées, idempotentes vs mutatives).
Analyse du correcteur : Parallélisation conditionnelle et réutilisation des connexions sont deux stratégies valides. Le gain annoncé de 20 à 50 % par appel est non étayé et ne peut être garanti sans mesure.
Q169
2/4H2Horodatage UTC 2026-08-27T00:13:47Z · durée 41.3 s
SHA-256 : a71c79dc96cfef87775586e4ded291e92a2c28ccde6a2fe8ef6cf15aa762f3d4
Question
Décrire le principe d’idempotence dans une API et pourquoi il est important pour les retries.
Réponse de LANXAS AI ONE
Principe d’idempotence dans une API
Une opération est idempotente lorsqu’une requête identique, exécutée une ou plusieurs fois, produit exactement le même effet sur l’état du serveur et renvoie le même résultat. La notion se distingue de la sécurité : une méthode sûre ne modifie jamais l’état (GET), tandis qu’une méthode idempotente peut le modifier, mais de manière déterministe et réversible à l’identique. Dans les APIs RESTful, GET, PUT, DELETE et souvent PATCH sont conçues pour être idempotentes. Par exemple, l’appel PUT /ressources/42 avec le payload {« valeur » : 10} placera la ressource dans cet état, quel que soit le nombre de fois où la requête est envoyée. À l’inverse, POST est généralement non idempotente, car chaque appel génère une nouvelle création (une commande en plus, un doublon de transaction).
Importance pour les mécanismes de retry
Les réseaux sont intrinsèquement peu fiables. Un client peut ne pas recevoir de réponse en raison d’un timeout réseau, d’une interruption temporaire ou d’une réponse partielle. Les bibliothèques clientes et les proxies implémentent automatiquement des tentatives de retry pour pallier ces instabilités. Si une opération n’est pas idempotente, un retry automatique provoque des effets secondaires indésirables : doubles paiements, créations de doublons, corruption de données ou désynchronisation de l’état métier. En rendant une API idempotente, on garantit que le retry ne modifie pas le résultat final. Le système converge vers le même état cohérent, ce qui permet d’activer des politiques de retry agressives sans risque de duplication.
Mise en œuvre et limites
Pour les opérations intrinsèquement non idempotentes, on compense ce manque en ajoutant des clés d’idempotence. Le client génère un identifiant unique qu’il transmet dans un en-tête dédié (Idempotency-Key, X-Request-ID). Le serveur mémorise le résultat associé à cette clé et renvoie la réponse initiale sans réexécuter la logique métier. L’idempotence ne supprime pas tous les risques résiduels. Une implémentation naïve peut ignorer les conflits de lecture-écriture ou les conditions de concurrence. Il faut coupler l’idempotence à des mécanismes de verrouillage, de versions optimistes ou de transactions atomiques. De plus, les timeouts mal calibrés peuvent masquer des réponses tardives, rendant le client incapable de distinguer une requête en cours d’exécution d’une requête déjà traitée. Le serveur doit donc conserver les clés d’idempotence avec une durée de vie définie et purger les entrées obsolètes pour limiter l’impact mémoire.
Analyse du correcteur : Rôle des clés d'idempotence pour les retries bien identifié, mais une opération idempotente garantit le même effet voulu, pas nécessairement la même réponse ; elle n'est pas nécessairement réversible, PATCH n'est pas généralement idempotent et X-Request-ID n'est pas automatiquement une clé d'idempotence.
Q170
3/4H1Horodatage UTC 2026-08-27T00:14:35Z · durée 48.5 s
SHA-256 : 432c8e4851952f4dc4824e806d44ccea540d2dc04dbd45eec47c08604b073e46
Question
Expliquer le problème N+1 dans les ORM et une stratégie de correction.
Réponse de LANXAS AI ONE
Définition du problème N+1
Le problème N+1 est un anti-pattern d'exécution de requêtes dans les ORM (Object-Relational Mappers). Il se produit lorsqu'un ORM exécute une première requête pour récupérer une collection d'objets parents, puis N requêtes supplémentaires, une par objet parent, pour charger leurs relations associées. Le nombre total de requêtes vers la base de données devient donc 1 + N.
Mécanisme d'exécution
Exemple typique avec une relation Utilisateur (1) -> Posts (N) :
Requête 1 (exécution initiale) :
SELECT id, nom, email FROM utilisateurs;
Boucle de chargement différé (lazy loading) :
Pour chaque utilisateur récupéré, l'ORM déclenche automatiquement :
SELECT * FROM posts WHERE utilisateur_id = ?;
Cette deuxième requête est exécutée N fois, une fois par ligne de la première requête. Si la première requête retourne 100 utilisateurs, le nombre total de requêtes passe à 101.
Impact technique
- Latence accumulée : chaque requête supplémentaire engendre un aller-retour réseau vers le serveur de base de données
- Saturation du pool de connexions : les connexions restent ouvertes plus longtemps que nécessaire
- Surcharge CPU et IO : le moteur de base de données doit parser, optimiser et exécuter N requêtes quasi identiques
- Dégradation exponentielle : les performances chutent rapidement avec la croissance des données ou le nombre de requêtes concurrentes
- Consommation mémoire : chargement simultané de N résultats partiellement redondants
Stratégies de correction
1. Chargement hâtif par jointure (Eager Loading / JOIN)
Remplace N+1 requêtes par 1 ou 2 requêtes uniques utilisant LEFT JOIN ou INNER JOIN.
Exemple ORM :
session.query(Utilisateur).options(joinedload(Utilisateur.posts)).all()
Équivalent SQL :
SELECT u.id, u.nom, p.id AS post_id, p.titre FROM utilisateurs u LEFT JOIN posts p ON u.id = p.utilisateur_id;
Avantage : réduction drastique du nombre de requêtes
Limite : produit cartésien potentiellement massif si la relation 1:N a une forte cardinalité, ce qui augmente la charge mémoire et le temps de tri côté ORM
2. Chargement par lots (Batch Fetching / Chunking)
Regroupe les requêtes associées par palettes d'IDs plutôt que par ligne.
Exemple ORM :
session.query(Utilisateur).options(selectinload(Utilisateur.posts)).all()
L'ORM transforme les N requêtes en plusieurs requêtes de type :
SELECT * FROM posts WHERE utilisateur_id IN (1, 2, 3, ..., 100);
SELECT * FROM posts WHERE utilisateur_id IN (101, 102, ..., 200);
Avantage : équilibre entre nombre de requêtes et taille des résultats, sans produit cartésien
Limite : nécessite un paramétrage fin de la taille des chunks
3. Pattern Dataloader (regroupement asynchrone + cache)
Utilisé dans les architectures GraphQL ou les API asynchrones.
- Regroupe les demandes de chargement sur le même ID survenant dans le même cycle d'exécution
- Exécute une seule requête par ID distinct
- Mets en cache les résultats pour éviter les redondances
Avantage : élimine les doublons et réduit les allers-retours dans les scénarios concurrents
Limite : complexité de mise en œuvre, nécessite une architecture adaptée
4. Projections ciblées (DTOs / colonnes sélectionnées explicitement)
Ne charge que les champs strictement nécessaires.
Exemple :
session.query(Utilisateur.id, Utilisateur.nom, Post.titre).join(Post).all()
Réduit la charge réseau et mémoire, même si le nombre de requêtes n'est pas modifié. Utile en combinaison avec les stratégies précédentes.
Recommandations d'implémentation
- Activer les logs SQL en développement pour détecter les N+1 (ex. echo=True en SQLAlchemy, SHOW_QUERIES en Django, SQLNinja en Laravel)
- Privilégier les jointures pour les relations 1:1 ou 1:N de faible cardinalité
- Utiliser le batch fetching pour les relations 1:N de forte cardinalité
- Implémenter une pagination stricte pour limiter la valeur de N
- Valider les performances en environnement de staging avec des jeux de données réalistes et des charges concurrentes
- Documenter les stratégies de chargement choisies pour chaque endpoint ou service afin de garantir la cohérence technique
Analyse du correcteur : N+1 et eager/batch loading correctement expliqués. La dégradation est principalement linéaire avec N, non exponentielle, et un DataLoader regroupe normalement plusieurs identifiants en une requête plutôt que d'en exécuter une par identifiant distinct.
Q171
4/4H0Horodatage UTC 2026-08-27T00:15:02Z · durée 26.8 s
SHA-256 : 4107e511f19a5a566d469fcbede2666d348ee59f41216f107b45988e4013e497
Question
En Git, expliquer la différence conceptuelle entre merge et rebase et le risque d’un rebase sur un historique partagé.
Réponse de LANXAS AI ONE
La différence conceptuelle entre merge et rebase réside dans leur traitement de l'historique des commits et dans la structure du graphe Git.
Le merge conserve l'intégralité de l'historique tel qu'il a été produit. Lors de l'intégration d'une branche dans une autre, Git crée un nouveau commit de fusion qui relie les deux sommets de développement. Le graphe conserve les bifurcations et les jonctions, ce qui préserve la chronologie réelle des travaux parallèles et la traçabilité des contributions.
Le rebase déplace les commits de la branche courante pour les appliquer à la suite du sommet de la branche cible. Git recrée les commits un par un sur le nouveau point d'ancrage, ce qui supprime les commits de fusion et produit une ligne d'historique linéaire. L'historique est réécrit, ce qui simplifie la lecture séquentielle mais modifie les identifiants des commits.
Le risque principal d'un rebase sur un historique partagé provient de cette réécriture. Les commits réécrits obtiennent de nouveaux identifiants SHA. Si la branche concernée a déjà été poussée et consultée par d'autres développeurs, ceux-ci possèdent des copies locales basées sur les anciens SHAs. Appliquer un rebase sur une branche partagée nécessite un push forcé, ce qui impose aux collaborateurs de réaligner leur travail, souvent via un rebase local ou un reset. Cela génère des conflits redondants, des duplications de commits et une perte de contexte sur les intégrations précédentes. Les outils de revue de code, les scripts de déploiement et les références externes qui s'appuient sur les SHAs ou les merge commits peuvent également perdre leurs liens.
La pratique recommandée consiste à réserver le rebase aux branches locales ou privées, et à utiliser le merge pour les branches partagées ou publiées. Si une linéarisation est souhaitée sur une branche partagée, il est préférable d'opter pour un merge avec l'option --no-ff ou pour un squash-merge au moment de l'intégration finale, afin de préserver la stabilité du graphe partagé.
Analyse du correcteur : Merge, rebase et risque de réécriture d'un historique partagé correctement expliqués.
Q172
4/4H0Horodatage UTC 2026-08-27T00:15:20Z · durée 17.6 s
SHA-256 : 7c12b036139e111e4aca837eb87b22a0bac212a91a6cf2d458390b12c22901ca
Question
Écrire une condition SQL correcte pour sélectionner les lignes dont une colonne peut être NULL ; expliquer pourquoi `= NULL` est incorrect.
Réponse de LANXAS AI ONE
Condition correcte :
`WHERE colonne IS NULL`
Explication de l'erreur `= NULL` :
En SQL, `NULL` n'est pas une valeur mais la représentation d'une absence de donnée ou d'une valeur inconnue. Le moteur SQL applique une logique à trois états : `VRAI`, `FAUX` et `INCONNU`. Toute comparaison avec `NULL` via les opérateurs standards (`=`, `<>`, `<`, `>`, `<=`, `>=`) retourne systématiquement `INCONNU`. La clause `WHERE` ne sélectionne que les lignes dont l'expression évalue à `VRAI`. Puisque `colonne = NULL` produit toujours `INCONNU`, la condition ne correspond jamais et renvoie un jeu de résultats vide.
L'opérateur `IS NULL` est spécifiquement conçu pour tester la présence d'une absence de donnée. Il évalue correctement `VRAI` ou `FAUX` sans passer par le `INCONNU`, ce qui garantit un filtrage fiable.
Exemple complet :
`SELECT * FROM ma_table WHERE ma_colonne IS NULL;`
Analyse du correcteur : IS NULL et logique SQL à trois valeurs correctement expliqués.
Q173
4/4H0Horodatage UTC 2026-08-27T00:16:12Z · durée 51.9 s
SHA-256 : adfab7fdf7b6d4dc7d42bd7f3aab402453e00e7f6ec2f8a2b4797344bf95dc43
Question
Décrire ce qu’est une fuite mémoire dans un langage à ramasse-miettes et comment elle peut encore survenir.
Réponse de LANXAS AI ONE
Une fuite mémoire dans un langage à ramasse-miettes désigne la rétention prolongée ou permanente d’espace mémoire alloué par l’application alors que ce dernier n’est plus utilisé ni nécessaire au bon fonctionnement du programme. Dans un langage sans ramasse-miettes, la fuite survient lorsque le développeur alloue de la mémoire et oublie de la libérer explicitement. Dans un langage à ramasse-miettes, le mécanisme de collecte automatique est censé identifier et récupérer la mémoire occupée par les objets devenus inaccessibles depuis les racines du programme. Une fuite persiste donc lorsque des objets pourtant inutiles restent techniquement accessibles, empêchant le ramasse-miettes de les considérer comme collectables.
Le ramasse-miettes fonctionne généralement sur un modèle de traçage des références. Il part des racines connues (variables globales, piles d’appels, registres) et marque récursivement tous les objets atteignables. La mémoire non marquée est considérée comme morte et est récupérée. Une fuite survient dès que le programme maintient involontairement des références vers des objets qui ne devraient plus exister.
Les causes techniques principales sont les suivantes :
- Collections statiques ou à durée de vie prolongée : des tableaux, dictionnaires ou listes déclarés statiquement ou attachés à des singletons continuent de grandir sans politique d’éviction. Les éléments ajoutés restent référencés indéfiniment.
- Caches sans stratégie d’invalidation : un cache en mémoire vive conserve des données anciennes. Sans algorithme de type LRU, LFU ou TTL, la mémoire croît linéairement avec le nombre de requêtes.
- Gestionnaires d’événements et écouteurs non désabonnés : chaque ajout d’un listener conserve une référence forte vers l’objet cible. Si la désinscription n’est pas systématiquement exécutée, les objets ne sont jamais collectés.
- Fermetures capturant des scopes larges : une fonction imbriquée qui référence des variables externes maintient l’accès à des structures volumineuses, même si seule une petite partie est réellement utilisée.
- Références circulaires dans les algorithmes sans détection de cycles : les ramasse-miettes à comptage de références seuls ne peuvent pas récupérer des objets qui se référencent mutuellement. Les algorithmes modernes à traçage gèrent ce cas, mais une implémentation partielle ou un runtime legacy peut échouer.
- Interopérabilité avec du code natif : les liaisons FFI, JNI ou CG allouent de la mémoire hors pile gérée par le GC. Si le code natif ne libère pas explicitement ces blocs, la mémoire système s’épuise même si l’objet managé est collecté.
- Configuration ou limites du runtime : une taille de heap mal calibrée, des pauses de collecte trop fréquentes ou des paramètres de génération inadaptés peuvent provoquer une accumulation progressive ou un arrêt prématuré, simulant une fuite ou l’aggravant.
Il convient de distinguer la fuite mémoire au sens strict du GC de l’épuisement des ressources système. Un programme peut voir sa consommation mémoire augmenter sans qu’une vraie fuite GC ne soit présente, par exemple à cause de fragmentation du tas, d’allocation de pages système non retournées au noyau, ou de descripteurs de fichiers et de sockets non fermés. Ces derniers ne sont généralement pas gérés automatiquement par le ramasse-miettes et nécessitent une fermeture explicite ou des mécanismes de gestion de cycle de vie.
La détection repose sur l’analyse de snapshots mémoire, la comparaison de références avant et après des cycles de collecte, et l’observation de courbes de croissance non linéaires ou monotones malgré l’absence de charge. Les correctifs consistent à limiter la durée de vie des références, à implémenter des politiques d’éviction, à utiliser des références faibles ou fortes selon le besoin, et à externaliser les données volumineuses vers des flux ou du stockage disque.
Analyse du correcteur : Rétention d'objets atteignables et principales causes de fuite sous GC correctement décrites.
Q174
4/4H0Horodatage UTC 2026-08-27T00:17:00Z · durée 48.1 s
SHA-256 : 92d8ea7962d53c69dd6399829e537c466bc8df92632c7048994b94278b03852c
Question
Un test passe localement mais échoue aléatoirement en CI. Donner quatre causes techniques plausibles et une méthode d’investigation.
Réponse de LANXAS AI ONE
Causes techniques plausibles :
1. Conditions de concurrence et sensibilité aux délais : le code exploite des opérations asynchrones ou des threads. En local, le processeur est dédié et rapide, masquant les retards. En CI, le runner est partagé ou plus lent, ce qui modifie l'ordre d'exécution des tâches et provoque des échecs intermittents.
2. Différences d'environnement d'exécution : la version de l'interpréteur, des bibliothèques, du système d'exploitation, du fuseau horaire ou des variables d'environnement diffère entre la machine locale et le runner CI. Une dépendance non figée ou une configuration implicite peut faire varier le comportement.
3. Absence de déterminisme : le test repose sur des valeurs aléatoires sans graine fixe, sur l'horloge système, sur des appels réseau ou sur l'état d'une base de données partagée. Ces facteurs varient d'une exécution à l'autre en CI, rendant le résultat non reproductible.
4. Contraintes ressources et exécution parallèle : les runners CI allouent des ressources limitées (CPU, mémoire, bande passante). Le test peut subir des timeouts ou des erreurs d'allocation en CI. De plus, l'exécution parallèle des tests peut créer des conflits d'accès à des fichiers, sockets ou états globaux, absents en mode séquentiel local.
Méthode d'investigation : reproduction contrôlée en environnement CI isolé. Procéder en quatre étapes strictes : 1. Figer l'environnement en utilisant un conteneur Docker ou une machine virtuelle dont la configuration (OS, versions, variables, fuseau horaire) correspond exactement à celle du runner CI. 2. Réduire le non-déterminisme en désactivant le parallélisme, en figeant la graine aléatoire, en mockant les appels externes et en utilisant des données de test statiques. 3. Activer le logging détaillé en exécutant le test en mode verbeux, en capturant les journaux complets, les dumps de mémoire ou les traces de concurrence, et en les stockant en tant qu'artefacts CI. 4. Reproduire en boucle et isoler la cause racine en lançant le test en série jusqu'à l'échec, puis en identifiant la portion de code ou la condition limite responsable. Une fois reproduit localement, corriger le problème et valider avec une suite de passes consécutives avant de réactiver le parallélisme.
Analyse du correcteur : Quatre causes plausibles et méthode d'investigation structurée correctes.
Q175
4/4H0Horodatage UTC 2026-08-27T00:17:46Z · durée 46.2 s
SHA-256 : 54dee04558c58b483142d823e93a9696a8de297290331329ca3e53ca51b995b3
Question
Expliquer pourquoi des tests qui ne vérifient aucune assertion peuvent donner une fausse impression de qualité.
Réponse de LANXAS AI ONE
Des tests qui n’exécutent aucune assertion produisent une illusion de qualité pour plusieurs raisons techniques et organisationnelles. Voici les mécanismes en jeu.
1. Taux de réussite artificiellement élevé. Un test sans assertion ne possède pas de critère d’échec explicite. Il ne peut échouer que s’il lève une exception non capturée ou s’il provoque un crash du processus. Dans la majorité des cas, il s’exécute et se termine avec un code de retour zéro. Le rapport d’exécution affiche donc 100 % de réussite, même si le code testé est totalement défectueux.
2. Confusion entre exécution et vérification. L’exécution d’une fonction ne prouve pas qu’elle se comporte correctement. Une assertion valide un état, un retour, une exception ou un comportement collatéral. Sans elle, le test ne fait qu’accomplir un parcours de code. Il ne capture aucune information sur la justesse du résultat. La qualité perçue provient du nombre de tests, pas de leur capacité à détecter des anomalies.
3. Fausse confiance dans les pipelines d’intégration continue. Les systèmes CI/CD interprètent un statut vert comme une validation de la livraison. Si les rapports contiennent des tests sans assertion, le pipeline signale un passage réussi sans avoir réellement vérifié la conformité du logiciel. L’équipe interprète ce vert comme une garantie de stabilité, alors qu’il ne représente qu’un chemin d’exécution validé.
4. Masquage des régressions. Lorsqu’une modification introduit un bug, un test sans assertion ne le détecte pas. Le bug passe en production. À l’inverse, les tests bien structurés échouent dès que le comportement attendu est rompu. L’absence d’assertion supprime donc le filet de sécurité principal des tests. La suite de tests semble stable, mais elle perd sa fonction première.
5. Coût de maintenance sans valeur ajoutée. Chaque test sans assertion doit être écrit, exécuté, maintenu et intégré dans les rapports. Il consomme du temps de calcul et de revue sans apporter de couverture fonctionnelle. Plus le nombre de ces tests augmente, plus la suite devient lourde à exécuter, ce qui peut ralentir les livraisons tout en maintenant un indicateur de qualité trompeur.
Conséquences sur la qualité logicielle. La métrique de couverture ou le nombre de tests devient un indicateur de vanité. Il reflète l’activité de développement, pas la fiabilité du produit. Les équipes peuvent ralentir les corrections de bugs ou ignorer des anomalies critiques, car les rapports de tests ne les signalent pas. La confiance dans l’automatisation s’érode progressivement lorsque des bugs critiques passent inaperçus malgré une suite de tests verte.
Recommandations techniques. Chaque test doit contenir au moins une vérification explicite : état retourné, exception levée, appel de méthode vérifié par un mock, ou modification d’état observable. Les frameworks modernes permettent de vérifier les comportements sans assertion directe, mais ces vérifications comptent comme des assertions. Un test doit pouvoir échouer de manière déterministe. Si un test ne peut pas échouer, il n’est pas un test de validation, mais un script d’exemple ou un cas d’usage.
L’illusion de qualité naît du décalage entre l’exécution et la vérification. Un test sans assertion ne valide rien. Il ne mesure que la capacité du code à s’exécuter sans crash. La qualité réelle se mesure à la capacité des tests à détecter les écarts entre le comportement spécifié et le comportement implémenté.
Analyse du correcteur : Fausse confiance créée par des tests sans oracle correctement analysée.