Les arguments positionnels s'associent dans l'ordre de gauche à droite : a reçoit 1, b reçoit 3. b a une valeur par défaut de 2, mais comme une valeur lui est transmise, celle-ci a priorité. Les valeurs 4 et 5, non associées, sont regroupées en tuple dans args (avec un seul astérisque), donnant (4, 5). Aucun argument nommé n'étant transmis, kwargs est un dictionnaire vide {}, et non None. Croire que la valeur par défaut 2 subsiste pour b donnerait (1, 2, (3, 4, 5), {}), mais un argument positionnel a priorité sur la valeur par défaut. Ce qui entre dans args étant un tuple et non une liste, il n'y a pas non plus d'affichage entre crochets.
Q2 | Précision par mot-clé
Concernant l'appel suivant, transmettant par mot-clé un nom absent de la définition, que s'affiche-t-il ?
Un seul argument positionnel étant transmis, a reçoit 1, et b n'étant pas transmis, reste à sa valeur par défaut 2. Le paramètre nommé x n'existant pas dans la définition, l'argument nommé x=9 entre dans kwargs (avec deux astérisques) sous la forme {'x': 9}. Aucun argument positionnel n'étant en surplus, args est un tuple vide (). Croire que x=9 entre dans b donnerait (1, 9, (), {}), mais un argument nommé ne peut entrer que dans un paramètre formel de même nom. Croire que 9 entre dans args donnerait (1, 2, (9,), {}), mais une valeur transmise par nom n'est jamais reversée dans le surplus d'arguments positionnels. Confondre le rôle d'un astérisque (qui reçoit un dictionnaire) et de deux astérisques (qui recevraient un tuple) ferait croire que {'x': 9} entre dans args et que kwargs reste vide.
Q3 | Liste vide par défaut
On appelle 3 fois la fonction suivante, dont la valeur par défaut est une liste vide. Quelles sont les 3 lignes affichées ?
La valeur par défaut [] n'est créée qu'une seule fois, lors de l'exécution de la ligne def, et cette même liste reste conservée dans l'objet fonction. Une nouvelle liste vide n'est pas créée à chaque appel ; le résultat d'un append persiste donc jusqu'à l'appel suivant, si bien que la liste s'allonge : [1], [1, 2], [1, 2, 3]. Croire qu'une nouvelle liste est créée à chaque appel donnerait [1] / [2] / [3], mais c'est le comportement d'une version où lst=None par défaut serait reconstruite dans le corps de la fonction. Écrire par erreur le corps comme return lst.append(x) ferait afficher trois fois None, la valeur de retour d'append. Notons qu'en écrivant sur une seule ligne print(add(1), add(2), add(3)) au lieu de 3 lignes séparées, les trois pointent vers la même liste, si bien qu'après évaluation complète, [1, 2, 3] s'affiche trois fois.
Q4 | Partage de la valeur par défaut
On affiche l'attribut de l'objet fonction après 3 appels. Quel est le contenu affiché ?
L'objet fonction conserve ses valeurs par défaut sous forme de tuple dans un attribut appelé __defaults__. Seul lst possède une valeur par défaut parmi les paramètres, il en résulte donc un tuple à un élément, affiché avec une virgule finale. Son contenu est exactement la liste créée une seule fois lors de la ligne def, si bien que le résultat des append effectués lors des 3 appels y reste, donnant ([1, 2, 3],). Sans aucun appel, on aurait ([],), et le fait que les appels se répercutent ici illustre le danger des arguments par défaut mutables. __defaults__ étant lui-même un tuple, il ne s'affiche pas entre crochets, et la liste qu'il contient ne se transforme pas non plus en tuple.
Q5 | Évaluation de la valeur par défaut
On écrit une variable comme valeur par défaut, puis on modifie cette variable ensuite. Quelles sont les 2 lignes affichées ?
i = 5def h(x=i): return xi = 10print(h())print(h(i))
5 puis 5
5 puis 10
10 puis 10
10 puis 5
RéponseB. 5 puis 10
L'expression de la valeur par défaut n'est pas évaluée à chaque appel de la fonction, mais une seule fois au moment où la ligne def est exécutée. i valant alors 5, c'est 5 qui est conservé dans l'objet fonction. Modifier ensuite i pour lui donner 10 ne change pas la valeur par défaut déjà enregistrée, donc h() renvoie 5. En revanche, h(i) lit i au moment de l'appel pour le transmettre, donc renvoie sa valeur à ce moment-là, 10. Croire que la valeur par défaut est évaluée à chaque appel ferait obtenir 10 dans les deux lignes, et croire que la valeur transmise en argument est aussi remplacée par la valeur par défaut ferait obtenir 5 dans les deux lignes. Le piège des arguments par défaut mutables provient de cette même règle.
Q6 | Ordre des paramètres
On enregistre le code suivant dans un fichier et on l'exécute avec python3. Que se passe-t-il ?
def g(a=1, b): return a + bprint(g(2, 3))
Une SyntaxError survient à la définition, et aucune ligne n'est exécutée
4 s'affiche, et l'exécution se termine normalement jusqu'à la fin
Une TypeError survient au moment de l'appel, et l'exécution s'arrête là
5 s'affiche, et l'exécution se termine normalement jusqu'à la fin
RéponseA. Une SyntaxError survient à la définition, et aucune ligne n'est exécutée
On ne peut pas placer, après un paramètre formel avec valeur par défaut, un paramètre formel sans valeur par défaut. Cette règle étant vérifiée au niveau syntaxique, exécuter le fichier provoque d'abord une SyntaxError, avec le message non-default argument follows default argument. L'analyse syntaxique portant sur le fichier entier avant toute exécution, même la ligne du print n'est pas exécutée. C'est un moment de détection totalement différent d'une TypeError due à une erreur d'appel. g(2, 3) lui-même correspondant bien au nombre de paramètres formels, 5 devrait s'afficher si la définition était correcte, mais on n'y parvient jamais. Pour l'écrire correctement, il faut placer celui avec valeur par défaut en dernier, comme def g(b, a=1).
Q7 | Valeur de retour d'une fonction
On appelle comme suit deux fonctions se distinguant par la présence ou non de return. Quel est le contenu affiché ?
one se termine sans jamais passer par un return, sa valeur de retour est donc None. Ce n'est ni un tuple vide ni une chaîne vide qui est renvoyée. two aligne 1 et 2 séparés par une virgule après return, mais cela ne renvoie pas deux valeurs : cela renvoie un unique tuple (1, 2). type(two()).__name__ vaut donc tuple. Croire que plusieurs valeurs de retour sont regroupées en liste donnerait list, mais une expression alignée avec des virgules construit un tuple. Les erreurs rencontrées en tentant d'affecter la valeur de retour d'une fonction qui se contente d'afficher proviennent souvent de ce None.
Q8 | Appel avec déballage
Concernant le code suivant, qui appelle en plaçant un astérisque devant une liste, que s'affiche-t-il ?
Placer un seul astérisque devant une liste au moment de l'appel distribue son contenu un par un comme arguments positionnels. a reçoit donc 1, b reçoit 2, et le 3 restant est regroupé en tuple dans args (avec un seul astérisque), donnant (3,). Un tuple à un seul élément porte une virgule finale. Aucun argument nommé n'étant transmis, kwargs est un dictionnaire vide {}, et non None. En oubliant l'astérisque et en écrivant f(vals), la liste elle-même entre dans le premier paramètre formel, donnant ([1, 2, 3], 2, (), {}), le résultat changeant sans qu'aucune erreur ne se produise. Ce qui entre dans args étant un tuple, il n'y a pas non plus d'affichage entre crochets.
Q9 | Traitement des annotations de type
On appelle la fonction suivante avec une valeur d'un type différent de celui écrit dans l'annotation. Que se passe-t-il ?
文字列 s'affiche à la ligne 1, et <class 'int'> à la ligne 2
Une TypeError survient à la ligne 1, indiquant qu'un type différent de l'annotation ne peut pas être transmis
文字列 s'affiche à la ligne 1, et int à la ligne 2
Une ValueError survient à la ligne 1, indiquant l'impossibilité de convertir en int
RéponseA. 文字列 s'affiche à la ligne 1, et <class 'int'> à la ligne 2
Une annotation n'est enregistrée que dans un dictionnaire de l'objet fonction appelé __annotations__ ; aucune vérification de type n'a lieu à l'exécution. Même si int est écrit pour a, on peut transmettre une chaîne, et même si str est écrit pour b, on peut transmettre un entier. Même avec bool écrit pour la valeur de retour, ce qui est réellement renvoyé est la chaîne reçue telle quelle. La ligne 1 affiche donc directement 文字列. Ce qui se trouve dans __annotations__['a'] à la ligne 2 est l'objet type int, donc l'afficher donne <class 'int'>. Il ne s'affiche pas simplement les 3 lettres int. Pour vérifier réellement les types, il faut utiliser un outil de vérification externe, ou effectuer soi-même la vérification dans le corps de la fonction.
Q10 | Comment corriger la valeur par défaut
Concernant le code suivant, réécrit avec None comme valeur par défaut, quelles sont les 3 lignes affichées ?
def add2(x, lst=None): if lst is None: lst = [] lst.append(x) return lstprint(add2(1))print(add2(2))print(add2(3, [9]))
Dans l'ordre : [1] / [1, 2] / [9, 3]
Dans l'ordre : [1] / [2] / [9, 3]
Dans l'ordre : [1] / [2] / [3, 9]
Dans l'ordre : [1] / [2] / [3]
RéponseB. Dans l'ordre : [1] / [2] / [9, 3]
La méthode classique consiste à mettre None comme valeur par défaut, et à vérifier au début du corps si c'est None pour créer une nouvelle liste vide dans ce cas. None étant une valeur non modifiable, même s'il n'est évalué qu'une fois, aucun problème de partage ne survient. Au premier et au deuxième appel où lst est omis, une liste vide distincte est créée à chaque fois, donnant [1] et [2], sans que les éléments de l'appel précédent ne restent. Au troisième appel, l'appelant transmet [9], donc lst is None est faux, et 3 est ajouté à la fin de la liste transmise, donnant [9, 3]. Sans la réécriture, en gardant lst=[], au deuxième appel omis, les éléments du précédent resteraient, donnant [1, 2]. Ignorer la liste transmise par l'appelant et créer systématiquement une nouvelle liste donnerait [1] / [2] / [3], perdant l'avantage de cette méthode classique, qui est d'« utiliser ce qui est transmis quand c'est le cas ». En remplaçant l'append final par un insert(0, x) en tête, le troisième résultat deviendrait [3, 9].
Q11 | Ordre de recherche des noms
Quel est l'ordre correct LEGB dans lequel Python recherche un nom de variable ?
Recherche dans l'ordre : fonction en cours d'exécution, sommet du module, fonction englobante, natif
Recherche dans l'ordre : fonction en cours d'exécution, fonction englobante, sommet du module, natif
Recherche dans l'ordre : sommet du module, natif, fonction en cours d'exécution, fonction englobante
Recherche dans l'ordre : natif, sommet du module, fonction englobante, fonction en cours d'exécution
RéponseB. Recherche dans l'ordre : fonction en cours d'exécution, fonction englobante, sommet du module, natif
LEGB est l'acronyme de Local, Enclosing, Global, Built-in, exprimant une recherche allant du niveau le plus intérieur vers l'extérieur. On regarde d'abord à l'intérieur de la fonction en cours d'exécution, puis dans la fonction englobante qui la contient, puis au sommet du module, et enfin parmi les noms natifs comme len ou print. La recherche s'arrête dès qu'un nom est trouvé, si bien qu'utiliser le même nom à l'intérieur masque le nom externe. Chercher de l'extérieur vers l'intérieur ferait qu'une variable locale créée dans une fonction perdrait face à une variable de même nom au sommet du module, ce qui ne correspond pas au comportement réel. Inverser l'ordre de Global et Enclosing est aussi une erreur fréquente, mais la fonction englobante est plus intérieure que le sommet du module.
Q12 | Affectation locale
Concernant le code suivant, qui affecte à l'intérieur d'une fonction un nom identique à celui de l'extérieur, quelles sont les 2 lignes affichées ?
x = 'global'def f(): x = 'local' print(x)f()print(x)
local puis local
local puis global
global puis global
global puis local
RéponseB. local puis global
Affecter à un nom à l'intérieur d'une fonction crée ce nom comme une nouvelle variable locale, sans affecter en rien la variable de même nom au sommet du module. Le print(x) à l'intérieur de la fonction lit le x local et affiche donc local, mais le print(x) après la sortie de la fonction lit le x du sommet du module et affiche global. Croire que l'affectation à l'intérieur de la fonction se répercute à l'extérieur ferait obtenir local dans les deux lignes, mais cela nécessiterait de déclarer global x en tête de la fonction. À l'inverse, croire que l'affectation interne est ignorée et que la valeur externe est toujours lue ferait obtenir global dans les deux lignes, mais une fois l'affectation faite, la variable locale existe bel et bien. Cette asymétrie, où « la lecture atteint l'extérieur mais l'écriture ne l'atteint pas », est la base de la portée en Python.
Q13 | nonlocal
Dans le code suivant, où une fonction interne tente de modifier une variable externe, que s'affiche-t-il ?
def outer(): n = 1 def inner(): nonlocal n n = 2 inner() return ndef outer2(): n = 1 def inner(): n = 2 inner() return nprint(outer(), outer2())
1 2
2 2
2 1
1 1
RéponseC. 2 1
L'inner de outer contient nonlocal n, donc l'affectation à n atteint le n de la fonction outer, immédiatement à l'extérieur. Après l'appel à inner, le n renvoyé par outer vaut 2. L'inner de outer2 n'a pas cette déclaration, donc n = 2 se contente de créer une nouvelle variable locale dans inner, et le n de outer2 reste inchangé à 1. Le résultat affiché est donc 2 1. Croire que les deux atteignent l'extérieur donnerait 2 2, et croire qu'aucun des deux ne l'atteint donnerait 1 1, mais c'est bien la seule ligne nonlocal qui crée la différence. Notons que nonlocal ne peut être utilisé que si une variable de même nom existe déjà dans la fonction englobante, et ne peut pas désigner une variable du sommet du module. Pour modifier une variable de plus haut niveau, on utilise global.
Q14 | Erreur de variable non liée
On lit un nom, à une ligne, qui fait l'objet d'une affectation à une ligne ultérieure. Que se passe-t-il en exécutant le code suivant ?
z = 100def bad(): print(z) z = 1bad()
Une UnboundLocalError survient, rien ne s'affiche, et l'exécution s'arrête
100 s'affiche, et l'exécution se termine normalement jusqu'à la fin
None s'affiche, et l'exécution se termine normalement jusqu'à la fin
1 s'affiche, et l'exécution se termine normalement jusqu'à la fin
RéponseA. Une UnboundLocalError survient, rien ne s'affiche, et l'exécution s'arrête
Python détermine, au moment de compiler le corps d'une fonction, que tout nom faisant l'objet d'une affectation à l'intérieur de cette fonction est une variable locale. Ce jugement porte sur la fonction entière, sans tenir compte de l'ordre des lignes. Dans bad, comme il y a une affectation z = 1 à une ligne ultérieure, z est traité comme une variable locale de cette fonction. À l'étape print(z), aucune valeur n'y est encore entrée, d'où une UnboundLocalError. Le libellé exact du message de cette exception peut varier selon la version de Python, mais le nom du type reste UnboundLocalError. Le z = 100 du sommet n'est pas référencé. Croire que, comme la ligne se trouve avant l'affectation, c'est la variable externe qui est lue, ferait obtenir 100, mais Python ne fonctionne pas ainsi. Une variable locale sans valeur ne devient pas non plus None.
Q15 | Déclaration global
Dans le code suivant, alignant une fonction avec déclaration global et une fonction sans, que s'affiche-t-il ?
n = 10def f(): global n n = 20 return ndef g(): n = 30 return nprint(f(), g(), n)
20 30 30
20 30 20
20 30 10
10 30 10
RéponseB. 20 30 20
f contient global n, donc n = 20 modifie le n du sommet du module. f renvoie lui-même 20, et le n du sommet devient aussi 20. g n'a pas cette déclaration, donc n = 30 se contente de créer une variable locale dans g, sans affecter le n du sommet. g renvoie sa propre valeur locale, 30. Le n lu par le dernier print est celui du sommet, qui vaut 20 puisque f l'a modifié. Croire que l'affectation de g atteint aussi l'extérieur donnerait 20 30 30, et croire que global n'a aucun effet donnerait 20 30 10. Il faut aussi noter que la déclaration global est une instruction indiquant que « l'affectation à ce nom doit atteindre le sommet » ; pour simplement lire une valeur, aucune déclaration n'est nécessaire.
Q16 | L'effet de __all__
On prépare le mymod.py suivant, puis on écrit from mymod import * dans un autre fichier. Quels noms sont importés du côté appelant ?
Seul pub est importé, other et hello ne le sont pas
RéponseD. Seul pub est importé, other et hello ne le sont pas
Une importation par astérisque importe normalement tous les noms ne commençant pas par un tiret bas, mais si le module possède une liste de chaînes appelée __all__, l'importation se limite aux noms qui y figurent. Dans cet exemple, __all__ ne contient que pub, donc ni other ni hello ne sont importés. other ne commence pas par un tiret bas, mais dès lors que __all__ existe, cette liste a priorité. Le fait d'être une fonction ne lui accorde pas de traitement particulier ; hello n'est donc pas non plus importé. __all__ n'a d'effet que sur une importation par astérisque : une importation nommée comme from mymod import hello, ou l'usage import mymod suivi de mymod.hello(), ne sont pas gênés.
Q17 | La valeur de __name__
Concernant le mymod2.py suivant, quelle est la combinaison des valeurs de __name__ selon qu'on l'exécute directement avec python3 mymod2.py, ou qu'on l'importe depuis un autre fichier avec import mymod2 ?
En exécution directe, mymod2 ; à l'import, __main__
En exécution directe comme à l'import, les deux valent __main__
En exécution directe, __main__ ; à l'import, mymod2
En exécution directe comme à l'import, les deux valent mymod2
RéponseC. En exécution directe, __main__ ; à l'import, mymod2
Lorsqu'on exécute directement ce fichier avec python3, __name__ contient '__main__'. Lorsqu'il est importé depuis un autre fichier, il contient le nom du fichier sans extension, 'mymod2'. En exécution directe, deux lignes s'affichent donc, mymod2: __main__ et 直接実行 (exécution directe), tandis qu'à l'importation, une seule ligne s'affiche, mymod2: mymod2. if __name__ == '__main__': est l'écriture classique qui exploite cette différence pour isoler « le traitement à exécuter uniquement en exécution directe ». Inverser la combinaison ferait exécuter le traitement de vérification au moment de l'import, ce qui contredirait l'objectif de cette écriture. Si les deux cas donnaient toujours la même valeur, aucune distinction ne serait possible.
Q18 | sys.argv
En exécutant le args.py suivant avec la commande python3 args.py foo bar, quelles sont les 2 lignes affichées ?
sys.argv est une liste alignant les valeurs transmises depuis la ligne de commande, dont le premier élément est le nom du script lui-même. Dans cet exemple, on obtient une liste à 3 éléments comme ['args.py', 'foo', 'bar'], donc len vaut 3. sys.argv[1:], qui extrait à partir de l'indice 1, donne ['foo', 'bar'], sans le nom du script. Croire que le nombre d'arguments transmis correspond directement à la longueur donnerait 2, en oubliant de compter le nom du script en tête. Croire que sys.argv[1:] vaut ['args.py', 'foo'] correspondrait à une erreur sur la position de départ du découpage. En exécutant sans transmettre aucun argument, la longueur serait de 1, et sys.argv[1:] serait une liste vide.
Q19 | Rechargement de module
Quel est le comportement correct lorsqu'un même module est importé deux fois dans un même programme ?
La seconde fois également, le corps du module est relu depuis le début et réexécuté
La seconde fois, un objet distinct est créé, et l'état n'est plus partagé
La seconde fois, le cache est utilisé, et le corps du module n'est pas réexécuté
La seconde fois, une ImportError survient, un même nom ne pouvant être utilisé qu'une fois
RéponseC. La seconde fois, le cache est utilisé, et le corps du module n'est pas réexécuté
Un module déjà chargé une fois est mis en cache dans un dictionnaire appelé sys.modules. Le second import se contente de relier ce nom au même objet module déjà enregistré, sans relire le fichier ni réexécuter le corps. On peut le vérifier en plaçant un print au sommet du module : même importé deux fois, il ne s'affiche qu'une seule fois. Même en important sous un alias différent, c'est le même objet qui est désigné. Ainsi, si l'on modifie une variable d'un module, sa valeur modifiée sera visible depuis tous les autres endroits qui importent ce module. Importer deux fois le même nom ne provoque pas d'erreur, et pour relire le corps pendant le développement, il faut utiliser explicitement les fonctionnalités du module importlib.
Q20 | Détection d'un environnement virtuel
Quelle méthode est appropriée pour déterminer si le Python en cours d'exécution se trouve dans un environnement virtuel créé avec venv ?
Si sys.prefix n'est pas défini, on peut en déduire qu'on est dans un environnement virtuel
Si sys.prefix et sys.base_prefix coïncident, on peut en déduire qu'on est dans un environnement virtuel
Si sys.base_prefix est défini, on peut en déduire qu'on est dans un environnement virtuel
Si sys.prefix et sys.base_prefix diffèrent, on peut en déduire qu'on est dans un environnement virtuel
RéponseD. Si sys.prefix et sys.base_prefix diffèrent, on peut en déduire qu'on est dans un environnement virtuel
sys.prefix désigne l'emplacement où est installé le Python en cours d'exécution, et sys.base_prefix désigne l'emplacement du Python d'origine dont il est issu. En dehors d'un environnement virtuel, les deux ont la même valeur, mais à l'intérieur d'un environnement créé avec python3 -m venv, sys.prefix pointe vers le répertoire de l'environnement créé, tandis que sys.base_prefix continue de pointer vers le Python d'origine, si bien que les deux diffèrent. C'est la présence ou l'absence de cette différence qui sert d'indice pour la détection. Dire qu'on est dans un environnement virtuel lorsqu'ils coïncident est la condition inverse. sys.base_prefix est toujours défini, même en dehors d'un environnement virtuel, donc sa simple présence ne permet pas de trancher. sys.prefix est également toujours défini. Le chemin concret ou la version de pip diffèrent selon l'environnement ; il suffit de retenir cette seule relation.
Q21 | Valeur de retour de write
Concernant le code suivant, qui récupère la valeur de retour d'une méthode d'écriture, quel est le contenu affiché ?
with open('/tmp/sample.txt', 'w') as f: n = f.write("one\ntwo\nthree\n")print(n)
None s'affiche
3 s'affiche
14 s'affiche
11 s'affiche
RéponseC. 14 s'affiche
write renvoie comme valeur de retour le nombre de caractères écrits. "one\ntwo\nthree\n" comprend one (3 caractères), two (3 caractères) et three (5 caractères), soit 11 en tout, plus 3 sauts de ligne, ce qui donne un total de 14 caractères. Oublier de compter les sauts de ligne donnerait 11. Croire que le nombre de lignes écrites est renvoyé donnerait 3, mais ce que compte write est bien le nombre de caractères. Choisir None en croyant que rien n'est renvoyé est aussi une erreur fréquente, facile à confondre avec des méthodes comme append ou sort qui renvoient None, mais write renvoie bien un nombre. Notons que write n'ajoute pas de saut de ligne automatiquement ; pour séparer des lignes, il faut l'écrire soi-même dans la chaîne.
Q22 | readlines
On lit avec readlines un fichier contenant 3 lignes. Quel est le contenu affiché ?
with open('/tmp/sample.txt', 'w') as f: f.write("one\ntwo\nthree\n")with open('/tmp/sample.txt') as f: print(f.readlines())
['one\ntwo\nthree\n'] s'affiche
'one\ntwo\nthree\n' s'affiche
['one\n', 'two\n', 'three\n'] s'affiche
['one', 'two', 'three'] s'affiche
RéponseC. ['one\n', 'two\n', 'three\n'] s'affiche
readlines renvoie toutes les lignes sous forme de liste, mais le caractère de saut de ligne en fin de chaque élément reste tel quel. Le résultat est donc ['one\n', 'two\n', 'three\n']. Croire que les sauts de ligne sont retirés donnerait ['one', 'two', 'three'], mais pour obtenir cela, il faut appliquer rstrip à chaque élément. Renvoyer une seule chaîne sans découpage est le rôle de read ; readlines renvoie bien une liste. Croire que le fichier entier est simplement enveloppé en un seul élément sans traiter le saut de ligne comme séparateur donnerait ['one\ntwo\nthree\n'], mais readlines sépare bien un élément à chaque saut de ligne. En écrivant for line in f pour extraire ligne par ligne, chaque ligne obtenue porte également son saut de ligne.
Q23 | Deuxième read
On lit successivement deux fois depuis le même objet fichier. Quel est le contenu affiché ?
with open('/tmp/sample.txt') as f: a = f.read() b = f.read()print(len(a), b == '')
14 False s'affiche
14 True s'affiche
0 False s'affiche
0 True s'affiche
RéponseB. 14 True s'affiche
Un objet fichier mémorise sa position courante. Le premier read lit du début jusqu'à la fin, si bien que a reçoit 14 caractères, saut de ligne compris, et la position se déplace jusqu'à la fin. Le second read tente de lire à partir de la fin, mais il ne reste rien, donc une chaîne vide est renvoyée. len(a) vaut donc 14, et b == '' vaut True. Croire que le même contenu est renvoyé la seconde fois ferait obtenir False pour b == '', mais pour relire le même contenu, il faut ramener la position au début avec f.seek(0). len(a) vaudrait 0 si le fichier était vide, ou si la position était déjà à la fin. Cette situation, où rien ne s'affiche, prête facilement à confusion avec un échec de lecture, mais la cause réelle est que la position se trouve à la fin.
Q24 | with et close
On vérifie closed à l'intérieur et à l'extérieur du bloc with. Quelles sont les 2 lignes affichées ?
with open('/tmp/sample.txt') as f: print(f.closed)print(f.closed)
False puis False
False puis True
True puis False
True puis True
RéponseB. False puis True
À l'intérieur du bloc with, le fichier est encore ouvert, donc closed vaut False. En sortant du bloc, il est automatiquement fermé, si bien que closed vaut ensuite True. Le résultat affiché est donc False puis True. L'objectif de with est de garantir que cette fermeture s'exécute même si l'on oublie de l'écrire, y compris lorsqu'une exception survient à l'intérieur du bloc et provoque une sortie anticipée. En séparant par des virgules, on peut aussi ouvrir plusieurs fichiers regroupés dans une seule instruction with, et tous sont fermés de la même façon. Tenter de lire depuis un objet fichier fermé lève une ValueError ; il n'est donc pas possible de relire en dehors du with.
Q25 | Mode d'écriture
On ouvre le même fichier trois fois pour y écrire, puis on le lit. Quel est le contenu affiché ?
p = '/tmp/sample.txt'with open(p, 'w') as f: f.write('AAA')with open(p, 'a') as f: f.write('BBB')with open(p, 'w') as f: f.write('C')with open(p) as f: print(f.read())
Le contenu du fichier est AAABBB
Le contenu du fichier est AAABBBC
Le contenu du fichier est C
Le contenu du fichier est CAABBB
RéponseC. Le contenu du fichier est C
L'ouverture avec 'w' tronque le fichier à une longueur nulle au moment même de l'ouverture, indépendamment du contenu existant. En suivant l'ordre : le premier 'w' écrit AAA, puis le 'a' suivant, en ajout, donne AAABBB. Mais à l'instant de la troisième ouverture avec 'w', AAABBB disparaît, et C y est écrit, si bien qu'au final seul C subsiste. Croire que les trois ouvertures se font en 'a' donnerait AAABBBC, et croire que la dernière écriture est ignorée donnerait AAABBB. Écraser depuis le début sans tronquer correspond au comportement de 'r+', qui ne remplacerait que le premier caractère, donnant CAABBB. La troncature se produit au moment de l'ouverture et non au moment de l'écriture ; ouvrir simplement avec 'w' sans rien écrire vide donc le fichier. Pour ajouter, on utilise 'a' ; pour reconstruire, 'w' ; le mode exprime ainsi l'intention.
Q26 | Opération après fermeture
Que se passe-t-il en tentant de lire un objet fichier après un close ?
f = open('/tmp/sample.txt')f.close()print(f.read())
Une ValueError survient, indiquant une opération sur un fichier fermé
Une chaîne vide s'affiche, et l'exécution se termine normalement
Le fichier se rouvre automatiquement et tout son contenu s'affiche
Une OSError survient, indiquant l'impossibilité de rouvrir le fichier
RéponseA. Une ValueError survient, indiquant une opération sur un fichier fermé
Appeler read sur un objet fichier après un close provoque une ValueError, avec le message I/O operation on closed file. L'objet lui-même se souvenant qu'il a été fermé, il ne se rouvre pas automatiquement simplement parce qu'on en a besoin. Ce n'est pas non plus qu'une chaîne vide est renvoyée faute de contenu à lire ; cela correspond au comportement de read après avoir lu jusqu'à la fin. Une FileNotFoundError, par exemple lorsqu'un fichier est introuvable, appartient à la famille OSError, mais l'exception levée en manipulant un fichier fermé en est distincte : c'est une ValueError. Utiliser une instruction with permet structurellement d'éviter cette situation même en tentant de lire après la sortie du bloc.
Q27 | Lecture seule
Que se passe-t-il en tentant d'écrire dans un fichier ouvert sans préciser de mode ?
with open('/tmp/sample.txt', 'w') as f: f.write('data')with open('/tmp/sample.txt') as f: f.write('more')
Une io.UnsupportedOperation survient, indiquant l'impossibilité d'écrire
Une ValueError survient, indiquant que la spécification du mode est incorrecte
more est ajouté à la fin du contenu, et l'exécution se termine normalement
Le contenu est remplacé par more, et l'exécution se termine normalement
RéponseA. Une io.UnsupportedOperation survient, indiquant l'impossibilité d'écrire
Omettre le second argument de open correspond au mode lecture seule 'r'. Appeler write sur un objet fichier ouvert en lecture seule provoque une io.UnsupportedOperation, avec le message not writable. La syntaxe étant valide en elle-même, on ne s'aperçoit de l'erreur de mode qu'à l'exécution. Pour écraser, il faut rouvrir avec 'w' ; pour ajouter à la fin, avec 'a' ; pour lire et écrire à la fois, avec 'r+'. À l'inverse, appeler read sur un fichier ouvert avec 'w' ou 'a' provoque la même io.UnsupportedOperation, avec le message not readable. On peut dire que le mode déclare ce qui est autorisé sur cet objet fichier.
Q28 | seek et tell
Concernant le code suivant, qui lit seulement 5 caractères depuis le début puis vérifie la position, quel est le contenu affiché ?
with open('/tmp/sample.txt') as f: head = f.read(5) pos = f.tell() f.seek(0) print(repr(head), pos, f.tell())
'one\ntwo\nthree\n' 14 0
'one\nt' 5 0
'one\nt' 5 5
'one\nt' 0 0
RéponseB. 'one\nt' 5 0
En passant un nombre à read, on ne lit que ce nombre de caractères. Les 5 premiers caractères sont les 3 lettres de one, un saut de ligne, et le t de two, ce qui donne 'one\nt'. La position avance d'autant que l'on a lu, donc le tell qui suit renvoie 5. Le seek(0) suivant ramène la position au début, donc le dernier tell vaut 0. Croire que read ne déplace pas la position ferait obtenir 0 les deux fois pour tell, et croire que seek n'a pas d'effet ferait obtenir 5 les deux fois. Confondre l'argument de read, qui est un nombre de caractères et non de lignes, mènerait à choisir par erreur le contenu entier 'one\ntwo\nthree\n' et 14, en pensant lire jusqu'à la 5e ligne. seek(0) permet de relire depuis le début avec le même objet fichier.
Q29 | JSON et tuple
Concernant le code suivant, qui convertit en JSON un dictionnaire contenant un tuple avant de le relire, quelles sont les 2 lignes affichées ?
RéponseD. {"t": [1, 2]}, suivi de {'t': [1, 2]} list
JSON ne connaît que le tableau, et le type tuple n'y existe pas. Le tuple est donc converti en tableau dès l'étape de dumps, donnant la sortie {"t": [1, 2]}. À la relecture, le tableau est également restauré sous forme de liste, donc le type de back['t'] est list, et non tuple. L'information selon laquelle il s'agissait à l'origine d'un tuple n'est conservée nulle part, si bien que le type change lors de l'aller-retour. Pour le conserver sous forme de tuple, il faut appliquer soi-même tuple() après la relecture. Des parenthèses n'apparaissent jamais dans la sortie JSON, et le tuple n'est pas non plus converti en chaîne. Pour la même raison, utiliser un entier comme clé de dictionnaire le fait convertir en chaîne lors du dumps, sans redevenir un entier à la relecture.
Q30 | Clé JSON
Que se passe-t-il en tentant de convertir en JSON un dictionnaire à clés entières et un ensemble ?
{1: "a"} s'affiche à la ligne 1, puis [1, 2] à la ligne 2
Une TypeError survient à la ligne 1, la ligne 2 n'est pas exécutée
{"1": "a"} s'affiche à la ligne 1, puis une TypeError survient à la ligne 2
{"1": "a"} s'affiche à la ligne 1, puis [1, 2] à la ligne 2
RéponseC. {"1": "a"} s'affiche à la ligne 1, puis une TypeError survient à la ligne 2
Les clés d'un objet JSON devant obligatoirement être des chaînes, convertir en JSON un dictionnaire à clé entière transforme la clé en chaîne, affichant {"1": "a"}. C'est un traitement qui réussit normalement, sans exception ici. Il faut cependant noter qu'à la relecture, la clé reste une chaîne et ne redevient pas un entier. Aucun type JSON ne correspondant à un ensemble, tenter de le convertir avec dumps à la ligne 2 provoque une TypeError, avec le message Object of type set is not JSON serializable. Croire que l'ensemble est écrit comme un tableau donnerait [1, 2], mais pour obtenir cette conversion, il faut appliquer soi-même list() avant de le transmettre. Pour les valeurs contenant du japonais, par défaut, elles sont échappées à la sortie ; en passant False à ensure_ascii, elles sont produites avec les caractères tels quels.
Entraînement : répondez aux questions de cette page
Cet outil d'entraînement pose les questions dans un ordre aléatoire (il fonctionne lorsque JavaScript est activé). Vous pouvez de toute façon lire toutes les questions et explications ci-dessus.
* Les explications sont fournies à titre d'information pour l'étude. Le programme et le système des examens changent selon les années : vérifiez toujours les annonces officielles de l'organisme qui organise l'examen.
Cette page est une traduction du texte original japonais. En cas de différence entre la traduction et l'original, la version japonaise fait foi. Voir l'original en japonais