FAQ de programmation¶
Questions générales¶
Existe-t-il un débogueur de code source avec points d'arrêts, exécution pas-à-pas, etc. ?¶
Oui.
Plusieurs débogueurs sont décrits ci-dessous et la fonction native breakpoint()
permet d'utiliser n'importe lequel d'entre eux.
Le module pdb
est un débogueur console simple, mais parfaitement adapté à Python. Il fait partie de la bibliothèque standard de Python, sa documentation se trouve dans le manuel de référence
. Vous pouvez vous inspirer du code de pdb
pour écrire votre propre débogueur.
L'environnement de développement interactif IDLE, qui est fourni avec la distribution standard de Python (normalement disponible dans Tools/scripts/idle3) contient un débogueur graphique.
PythonWin est un environnement de développement intégré (EDI) Python qui embarque un débogueur graphique basé sur pdb
. Le débogueur PythonWin colore les points d'arrêts et possède quelques fonctionnalités sympathiques, comme la possibilité de déboguer des programmes développés sans PythonWin. PythonWin est disponible dans le projet pywin32 et fait partie de la distribution ActivePython.
Eric est un EDI basé sur PyQt et l'outil d'édition Scintilla.
trepan3k est un débogueur semblable à GDB.
Visual Studio Code est un EDI qui contient des outils de débogage. Il sait interagir avec les outils de gestion de versions.
Il existe de nombreux EDI Python propriétaires qui embarquent un débogueur graphique, notamment :
Existe-t-il des outils pour aider à trouver des bogues ou faire de l'analyse statique de code ?¶
Oui.
Pylint et Pyflakes font des vérifications de base et vous aideront à trouver des bogues plus tôt.
Static type checkers such as Mypy, Pyre, and Pytype can check type hints in Python source code.
Comment créer un binaire autonome à partir d'un script Python ?¶
Pour créer un programme autonome, c'est-à-dire un programme que n'importe qui peut télécharger et exécuter sans avoir à installer une distribution Python au préalable, il n'est pas nécessaire de compiler du code Python en code C. Il existe en effet plusieurs outils qui déterminent les modules requis par un programme et lient ces modules avec un binaire Python pour produire un seul exécutable.
Un de ces outils est freeze, qui se trouve dans Tools/freeze de l'arborescence des sources de Python. Il convertit le code intermédiaire (bytecode) Python en tableaux C ; avec un compilateur C, vous pouvez intégrer tous vos modules dans un nouveau programme, qui est ensuite lié aux modules standards Python.
Il fonctionne en cherchant de manière récursive les instructions d'importation (sous les deux formes) dans le code source et en recherchant ces modules dans le chemin Python standard ainsi que dans le répertoire source (pour les modules natifs). Il transforme ensuite le code intermédiaire des modules écrits en Python en code C (des tableaux pré-remplis qui peuvent être transformés en objets code à l'aide du module marshal) et crée un fichier de configuration personnalisé qui contient uniquement les modules natifs qui sont réellement utilisés dans le programme. Il compile ensuite le code C généré et le lie au reste de l'interpréteur Python pour former un binaire autonome qui fait exactement la même chose que le script.
Voici quelques paquets qui permettent de créer des exécutables en ligne de commande comme graphiques :
Nuitka (multiplateformes) ;
PyInstaller (Cross-platform)
PyOxidizer (multi-plateforme) ;
cx_Freeze (multi-plateforme) ;
py2app (uniquement pour macOS) ;
py2exe (uniquement pour Windows).
Existe-t-il des normes de développement ou un guide de style pour écrire des programmes Python ?¶
Oui. Le style de développement que les modules de la bibliothèque standard doivent obligatoirement respecter est documenté dans la PEP 8.
Fondamentaux¶
Pourquoi une UnboundLocalError est levée alors qu'une variable a une valeur ?¶
Il est parfois surprenant d'obtenir une UnboundLocalError
dans du code jusqu'à présent correct, quand celui-ci est modifié en ajoutant une instruction d'affectation quelque part dans le corps d'une fonction.
Le code suivant :
>>> x = 10
>>> def bar():
... print(x)
...
>>> bar()
10
fonctionne, mais le suivant :
>>> x = 10
>>> def foo():
... print(x)
... x += 1
results in an UnboundLocalError
:
>>> foo()
Traceback (most recent call last):
...
UnboundLocalError: local variable 'x' referenced before assignment
Cela est dû au fait que, quand une variable est affectée dans un contexte, cette variable devient locale à ce contexte et remplace toute variable du même nom du contexte appelant. Vu que la dernière instruction dans foo affecte une nouvelle valeur à x
, le compilateur la traite comme une nouvelle variable. Par conséquent, quand le print(x)
essaye d'afficher la variable non initialisée, une erreur se produit.
Dans l'exemple ci-dessus, la variable du contexte appelant reste accessible en la déclarant globale :
>>> x = 10
>>> def foobar():
... global x
... print(x)
... x += 1
...
>>> foobar()
10
Cette déclaration explicite est obligatoire pour se rappeler que (contrairement au cas à peu près similaire avec des variables de classe et d'instance), c'est la valeur de la variable du contexte appelant qui est modifiée :
>>> print(x)
11
Une alternative dans un contexte imbriqué consiste à utiliser le mot-clé nonlocal
:
>>> def foo():
... x = 10
... def bar():
... nonlocal x
... print(x)
... x += 1
... bar()
... print(x)
...
>>> foo()
10
11
Quelles sont les règles pour les variables locales et globales en Python ?¶
En Python, si une variable n'est pas modifiée dans une fonction mais seulement lue, elle est implicitement considérée comme globale. Si une valeur lui est affectée, elle est considérée locale (sauf si elle est explicitement déclarée globale).
Bien que surprenant au premier abord, ce choix s'explique facilement. D'une part, exiger global
pour des variables affectées est une protection contre des effets de bord inattendus. D'autre part, si global
était obligatoire pour toutes les références à des objets globaux, il faudrait mettre global
partout, car il faudrait dans ce cas déclarer globale chaque référence à une fonction native ou à un composant d'un module importé. Le codé serait alors truffé de déclarations global
, ce qui nuirait à leur raison d'être : identifier les effets de bords.
Pourquoi des expressions lambda définies dans une boucle avec des valeurs différentes retournent-elles le même résultat ?¶
Supposons que l'on utilise une boucle itérative pour définir des expressions lambda (voire des fonctions) différentes, par exemple :
>>> squares = []
>>> for x in range(5):
... squares.append(lambda: x**2)
Le code précédent crée une liste de 5 expressions lambda qui calculent chacune x**2
. En les exécutant, on pourrait s'attendre à obtenir 0
, 1
, 4
, 9
et 16
. Elles renvoient en réalité toutes 16
:
>>> squares[2]()
16
>>> squares[4]()
16
Ceci s'explique par le fait que x
n'est pas une variable locale aux expressions, mais est définie dans le contexte appelant. Elle est lue à l'appel de l'expression lambda – et non au moment où cette expression est définie. À la fin de la boucle, x
vaut 4
, donc toutes les fonctions renvoient 4*2
, c.-à-d. 16
. Ceci se vérifie également en changeant la valeur de x
et en constatant que les résultats sont modifiés :
>>> x = 8
>>> squares[2]()
64
Pour éviter ce phénomène, les valeurs doivent être stockées dans des variables locales aux expressions lambda pour que celles-ci ne se basent plus sur la variable globale x
:
>>> squares = []
>>> for x in range(5):
... squares.append(lambda n=x: n**2)
Dans ce code, n=x
crée une nouvelle variable n
, locale à l'expression. Cette variable est évaluée quand l'expression est définie donc n
a la même valeur que x
à ce moment. La valeur de n
est donc 0
dans la première lambda, 1
dans la deuxième, 2
dans la troisième et ainsi de suite. Chaque expression lambda renvoie donc le résultat correct :
>>> squares[2]()
4
>>> squares[4]()
16
Ce comportement n'est pas propre aux expressions lambda, mais s'applique aussi aux fonctions normales.
Quelles sont les « bonnes pratiques » pour utiliser import dans un module ?¶
De manière générale, il ne faut pas faire from modulename import *
. Ceci encombre l'espace de nommage de l'importateur et rend la détection de noms non-définis beaucoup plus ardue pour les analyseurs de code.
Les modules doivent être importés au début d'un fichier. Ceci permet d'afficher clairement de quels modules le code à besoin et évite de se demander si le module est dans le contexte. Faire un seul import
par ligne rend l'ajout et la suppression d'une importation de module plus aisé, mais importer plusieurs modules sur une même ligne prend moins d'espace.
Il est recommandé d'importer les modules dans l'ordre suivant :
les modules de la bibliothèque standard — p. ex.
sys
,os
,getopt
,re
les modules externes (tout ce qui est installé dans le dossier site-packages de Python) — par exemple
dateutil
,requests
,PIL.Image
les modules développés en local
Il est parfois nécessaire de déplacer des importations dans une fonction ou une classe pour éviter les problèmes d'importations circulaires. Comme le souligne Gordon McMillan :
Il n'y a aucun souci à faire des importations circulaires tant que les deux modules utilisent la forme
import <module>
. Ça ne pose problème que si le second module cherche à récupérer un nom du premier module ("from module import name") et que l'importation est dans l'espace de nommage du fichier. Les noms du premier module ne sont en effet pas encore disponibles car le premier module est occupé à importer le second.
Dans ce cas, si le second module n'est utilisé que dans une fonction, l'importation peut facilement être déplacée dans cette fonction. Au moment où l'importation sera appelée, le premier module aura fini de s'initialiser et le second pourra faire son importation.
Il peut parfois être nécessaire de déplacer des importations de modules hors de l'espace de plus haut niveau du code si certains de ces modules dépendent de la machine utilisée. Dans ce cas de figure, il est parfois impossible d'importer tous les modules au début du fichier. Dans ce cas, il est recommandé d'importer les modules adéquats dans le code spécifique à la machine.
Les importations ne devraient être déplacées dans un espace de nommage local, comme dans la définition d'une fonction, que si cela est nécessaire pour résoudre un problème comme éviter des dépendances circulaires ou réduire le temps d'initialisation d'un module. Cette technique est particulièrement utile si la majorité des importations est superflue selon le flux d'exécution du programme. Il est également pertinent de déplacer des importations dans une fonction si le module n'est utilisé qu'au sein de cette fonction. Le premier chargement d'un module peut être coûteux à cause du coût fixe d'initialisation d'un module, mais charger un module plusieurs fois est quasiment gratuit, cela ne coûte que quelques indirections dans un dictionnaire. Même si le nom du module est sorti du contexte courant, le module est probablement disponible dans sys.modules
.
Comment passer des paramètres optionnels ou nommés d'une fonction à l'autre ?¶
Il faut récupérer les arguments en utilisant les sélecteurs *
et **
dans la liste des paramètres de la fonction ; ceci donne les arguments positionnels sous la forme d'un n-uplet et les arguments nommés sous forme de dictionnaire. Ces arguments peuvent être passés à une autre fonction en utilisant *
et **
:
def f(x, *args, **kwargs):
...
kwargs['width'] = '14.3c'
...
g(x, *args, **kwargs)
Quelle est la différence entre les arguments et les paramètres ?¶
Les paramètres sont les noms qui apparaissent dans une définition de fonction, alors que les arguments sont les valeurs qui sont réellement passées à une fonction lors de l'appel de celle-ci. Les paramètres définissent les types d'arguments qu'une fonction accepte. Ainsi, avec la définition de fonction suivante :
def func(foo, bar=None, **kwargs):
pass
foo, bar et kwargs sont des paramètres de func
. Mais à l'appel de func
avec, par exemple :
func(42, bar=314, extra=somevar)
les valeurs 42
, 314
, et somevar
sont des arguments.
Pourquoi modifier la liste 'y' modifie aussi la liste 'x' ?¶
Si vous avez écrit du code comme :
>>> x = []
>>> y = x
>>> y.append(10)
>>> y
[10]
>>> x
[10]
vous vous demandez peut-être pourquoi l'ajout d'un élément à y
a aussi changé x
.
Il y a deux raisons qui conduisent à ce comportement :
Les variables ne sont que des noms qui font référence à des objets. La ligne
y = x
ne crée pas une copie de la liste — elle crée une nouvelle variabley
qui pointe sur le même objet quex
. Ceci signifie qu'il n'existe qu'un seul objet (la liste) auquelx
ety
font référence.Les listes sont des mutable, ce qui signifie que leur contenu peut être modifié.
After the call to append()
, the content of the mutable object has
changed from []
to [10]
. Since both the variables refer to the same
object, using either name accesses the modified value [10]
.
Si au contraire, on affecte un objet immuable à x
:
>>> x = 5 # ints are immutable
>>> y = x
>>> x = x + 1 # 5 can't be mutated, we are creating a new object here
>>> x
6
>>> y
5
on observe que x
et y
ne sont ici plus égales. Les entiers sont des immuables (immutable), et x = x + 1
ne change pas l'entier 5
en incrémentant sa valeur. Au contraire, un nouvel objet est créé (l'entier 6
) et affecté à x
(c'est-à-dire qu'on change l'objet auquel fait référence x
). Après cette affectation on a deux objets (les entiers 6
et 5
) et deux variables qui font référence à ces deux objets (x
fait désormais référence à 6
mais y
fait toujours référence à 5
).
Certaines opérations (par exemple, y.append(10)
et y.sort()
) modifient l'objet, alors que des opérations identiques en apparence (par exemple y = y + [10]
et sorted(y)()
) créent un nouvel objet. En général, en Python, une méthode qui modifie un objet renvoie None
(c'est même systématique dans la bibliothèque standard) pour éviter la confusion entre les deux opérations. Donc écrire par erreur y.sort()
en pensant obtenir une copie triée de y
donne None
, ce qui conduit très souvent le programme à générer une erreur facile à diagnostiquer.
Il existe cependant une classe d'opérations qui se comporte différemment selon le type : les opérateurs d'affectation incrémentaux. Par exemple, +=
modifie les listes mais pas les n-uplets ni les entiers (a_list += [1, 2, 3]
équivaut à a_list.extend([1, 2, 3])
et modifie a_list
, alors que some_tuple += (1, 2, 3)
et some_int += 1
créent de nouveaux objets).
En d'autres termes :
Il est possible d'appliquer des opérations qui modifient un objet mutable (
list
,dict
,set
, etc.) et toutes les variables qui y font référence verront le changement.Toutes les variables qui font référence à un objet immuable (
str
,int
,tuple
, etc.) renvoient la même valeur, mais les opérations qui transforment cette valeur en une nouvelle valeur renvoient toujours un nouvel objet.
L'opérateur is
ou la fonction native id()
permettent de savoir si deux variables font référence au même objet.
Comment écrire une fonction qui modifie ses paramètres ? (passage par référence)¶
En Python, les arguments sont passés comme des affectations de variables. Vu qu'une affectation crée des références à des objets, il n'y a pas de lien entre un argument dans l'appel de la fonction et sa définition, et donc pas de passage par référence en soi. Il y a cependant plusieurs façons d'en émuler un.
En renvoyant un n-uplet de résultats :
>>> def func1(a, b): ... a = 'new-value' # a and b are local names ... b = b + 1 # assigned to new objects ... return a, b # return new values ... >>> x, y = 'old-value', 99 >>> func1(x, y) ('new-value', 100)
C'est presque toujours la meilleure solution.
En utilisant des variables globales. Cette approche ne fonctionne pas dans des contextes à plusieurs fils d'exécution (elle n'est pas thread-safe), et n'est donc pas recommandée.
En passant un objet mutable (modifiable sur place) :
>>> def func2(a): ... a[0] = 'new-value' # 'a' references a mutable list ... a[1] = a[1] + 1 # changes a shared object ... >>> args = ['old-value', 99] >>> func2(args) >>> args ['new-value', 100]
En passant un dictionnaire, qui sera modifié :
>>> def func3(args): ... args['a'] = 'new-value' # args is a mutable dictionary ... args['b'] = args['b'] + 1 # change it in-place ... >>> args = {'a': 'old-value', 'b': 99} >>> func3(args) >>> args {'a': 'new-value', 'b': 100}
Ou regrouper les valeurs dans une instance de classe :
>>> class Namespace: ... def __init__(self, /, **args): ... for key, value in args.items(): ... setattr(self, key, value) ... >>> def func4(args): ... args.a = 'new-value' # args is a mutable Namespace ... args.b = args.b + 1 # change object in-place ... >>> args = Namespace(a='old-value', b=99) >>> func4(args) >>> vars(args) {'a': 'new-value', 'b': 100}
Faire quelque chose d'aussi compliqué est rarement une bonne idée.
La meilleure option reste de renvoyer un n-uplet contenant les différents résultats.
Comment construire une fonction d'ordre supérieur en Python ?¶
Deux possibilités : on peut utiliser des portées imbriquées ou bien des objets appelables. Par exemple, supposons que l'on souhaite définir linear(a, b)
qui renvoie une fonction f(x)
qui calcule la valeur a*x+b
. En utilisant les portées imbriquées :
def linear(a, b):
def result(x):
return a * x + b
return result
Ou en utilisant un objet appelable :
class linear:
def __init__(self, a, b):
self.a, self.b = a, b
def __call__(self, x):
return self.a * x + self.b
Dans les deux cas
taxes = linear(0.3, 2)
donne un objet appelable où taxes(10e6) == 0.3 * 10e6 + 2
.
L'approche par objet appelable a le désavantage d'être légèrement plus lente et de produire un code légèrement plus long. Cependant, il faut noter qu'une collection d'objet appelables peuvent partager leurs signatures par héritage :
class exponential(linear):
# __init__ inherited
def __call__(self, x):
return self.a * (x ** self.b)
Les objets peuvent encapsuler un état pour plusieurs méthodes :
class counter:
value = 0
def set(self, x):
self.value = x
def up(self):
self.value = self.value + 1
def down(self):
self.value = self.value - 1
count = counter()
inc, dec, reset = count.up, count.down, count.set
Ici inc()
, dec()
et reset()
agissent comme des fonctions partageant une même variable compteur.
Comment copier un objet en Python ?¶
En général, essayez copy.copy()
ou copy.deepcopy()
. Tous les objets ne peuvent pas être copiés, mais la plupart le peuvent.
Certains objets peuvent être copiés plus facilement que d'autres. Les dictionnaires ont une méthode copy()
:
newdict = olddict.copy()
Les séquences peuvent être copiées via la syntaxe des tranches :
new_l = l[:]
Comment récupérer les méthodes ou les attributs d'un objet ?¶
Pour une instance x
d'une classe définie par un utilisateur, dir(x)
renvoie une liste alphabétique des noms contenants les attributs de l'instance, et les attributs et méthodes définies par sa classe.
Comment un code peut-il obtenir le nom d'un objet ?¶
C'est impossible en général, parce qu'un objet n'a pas de nom à proprement parler. Schématiquement, l'affectation fait correspondre un nom à une valeur ; c'est vrai aussi pour les instructions def
et class
, à la différence près que, dans ce cas, la valeur est un appelable. Par exemple, dans le code suivant :
>>> class A:
... pass
...
>>> B = A
>>> a = B()
>>> b = a
>>> print(b)
<__main__.A object at 0x16D07CC>
>>> print(a)
<__main__.A object at 0x16D07CC>
Affirmer que la classe a un nom est discutable. Bien qu'elle soit liée à deux noms, et qu'elle soit appelée via le nom B
, l'instance créée déclare tout de même être une instance de la classe A
. De même, il est impossible de dire si le nom de l'instance est a
ou b
, les deux noms étant attachés à la même valeur.
De façon générale, une application ne devrait pas avoir besoin de « connaître le nom » d'une valeur particulière. À moins d'être délibérément en train d'écrire un programme introspectif, c'est souvent l'indication qu'un changement d'approche serait bénéfique.
Sur comp.lang.python, Fredrik Lundh a donné un jour une excellente analogie pour répondre à cette question :
C'est pareil que trouver le nom du chat qui traîne devant votre porte : le chat (objet) ne peut pas vous dire lui-même son nom, et il s'en moque un peu – alors le meilleur moyen de savoir comment il s'appelle est de demander à tous vos voisins (espaces de nommage) si c'est leur chat (objet)…
…et ne soyez pas surpris si vous découvrez qu'il est connu sous plusieurs noms, ou s'il n'a pas de nom du tout !
Qu'en est-il de la précédence de l'opérateur virgule ?¶
La virgule n'est pas un opérateur en Python. Observez le code suivant :
>>> "a" in "b", "a"
(False, 'a')
Comme la virgule n'est pas un opérateur, mais un séparateur entre deux expressions, l'expression ci-dessus est évaluée de la même façon que si vous aviez écrit :
("a" in "b"), "a"
et non :
"a" in ("b", "a")
Ceci est vrai pour tous les opérateurs d'affectation (=
, +=
etc). Ce ne sont pas vraiment des opérateurs mais plutôt des délimiteurs syntaxiques dans les instructions d'affectation.
Existe-t-il un équivalent à l'opérateur ternaire « ?: » du C ?¶
Oui. Sa syntaxe est la suivante :
[on_true] if [expression] else [on_false]
x, y = 50, 25
small = x if x < y else y
Avant l'introduction de cette syntaxe dans Python 2.5, il était courant d'utiliser les opérateurs de logique :
[expression] and [on_true] or [on_false]
Cet idiome est dangereux, car il donne un résultat erroné quand on_true a la valeur booléenne fausse. Il faut donc toujours utiliser la forme ... if ... else ...
.
Est-il possible d'écrire des programmes obscurcis (obfuscated) d'une ligne en Python ?¶
Oui. C'est souvent le cas en imbriquant des lambda
dans des lambda
. Par exemple les trois morceaux de code suivants, légèrement adaptés par Ulf Bartelt :
from functools import reduce
# Primes < 1000
print(list(filter(None,map(lambda y:y*reduce(lambda x,y:x*y!=0,
map(lambda x,y=y:y%x,range(2,int(pow(y,0.5)+1))),1),range(2,1000)))))
# First 10 Fibonacci numbers
print(list(map(lambda x,f=lambda x,f:(f(x-1,f)+f(x-2,f)) if x>1 else 1:
f(x,f), range(10))))
# Mandelbrot set
print((lambda Ru,Ro,Iu,Io,IM,Sx,Sy:reduce(lambda x,y:x+'\n'+y,map(lambda y,
Iu=Iu,Io=Io,Ru=Ru,Ro=Ro,Sy=Sy,L=lambda yc,Iu=Iu,Io=Io,Ru=Ru,Ro=Ro,i=IM,
Sx=Sx,Sy=Sy:reduce(lambda x,y:x+y,map(lambda x,xc=Ru,yc=yc,Ru=Ru,Ro=Ro,
i=i,Sx=Sx,F=lambda xc,yc,x,y,k,f=lambda xc,yc,x,y,k,f:(k<=0)or (x*x+y*y
>=4.0) or 1+f(xc,yc,x*x-y*y+xc,2.0*x*y+yc,k-1,f):f(xc,yc,x,y,k,f):chr(
64+F(Ru+x*(Ro-Ru)/Sx,yc,0,0,i)),range(Sx))):L(Iu+y*(Io-Iu)/Sy),range(Sy
))))(-2.1, 0.7, -1.2, 1.2, 30, 80, 24))
# \___ ___/ \___ ___/ | | |__ lines on screen
# V V | |______ columns on screen
# | | |__________ maximum of "iterations"
# | |_________________ range on y axis
# |____________________________ range on x axis
Les enfants, ne faites pas ça chez vous !
Que signifie la barre oblique (/) dans la liste des paramètres d'une fonction ?¶
Une barre oblique dans la liste des arguments d'une fonction indique que les paramètres la précédant sont uniquement positionnels. Les paramètres uniquement positionnels ne peuvent pas être référencés par leur nom depuis l'extérieur. Lors de l'appel d'une fonction qui accepte des paramètres uniquement positionnels, les arguments sont affectés aux paramètres en fonction de leur position. Par exemple, la fonction divmod()
n'accepte que des paramètres uniquement positionnels. Sa documentation est la suivante :
>>> help(divmod)
Help on built-in function divmod in module builtins:
divmod(x, y, /)
Return the tuple (x//y, x%y). Invariant: div*y + mod == x.
La barre oblique à la fin de la liste des paramètres signifie que les trois paramètres sont uniquement positionnels. Et donc, appeler divmod()
avec des arguments nommés provoque une erreur :
>>> divmod(x=3, y=4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: divmod() takes no keyword arguments
Nombres et chaînes de caractères¶
Comment écrire des entiers hexadécimaux ou octaux ?¶
Pour écrire un entier octal, faites précéder la valeur octale par un zéro, puis un "o" majuscule ou minuscule. Par exemple pour affecter la valeur octale "10" (8 en décimal) à la variable "a", tapez :
>>> a = 0o10
>>> a
8
L'hexadécimal est tout aussi simple, faites précéder le nombre hexadécimal par un zéro, puis un "x" majuscule ou minuscule. Les nombres hexadécimaux peuvent être écrits en majuscules ou en minuscules. Par exemple, dans l'interpréteur Python :
>>> a = 0xa5
>>> a
165
>>> b = 0XB2
>>> b
178
Pourquoi -22 // 10
donne-t-il -3
?¶
Cela est principalement dû à la volonté que i % j
ait le même signe que j. Si vous voulez en plus que :
i == (i // j) * j + (i % j)
alors la division entière doit renvoyer l'entier inférieur. Le C impose également que cette égalité soit vérifiée, et donc les compilateurs qui tronquent i // j
ont besoin que i % j
ait le même signe que i
.
Il y a peu de cas d'utilisation réels pour i % j
quand j
est négatif. Quand j
est positif, il y en a beaucoup, et dans pratiquement tous, il est plus utile que i % j
soit >=0
. Si l'horloge affiche 10 h maintenant, qu'affichait-elle il y a 200 heures ? -190 % 12 == 2
est utile ; -190 % 12 == -10
est un bogue en puissance.
Pourquoi ai-je une erreur de syntaxe en essayant de lire un attribut d'un entier littéral ?¶
Essayer d'utiliser l'opérateur d'accès à un attribut sur un entier littéral lève une SyntaxError
car le point est compris comme un séparateur décimal en notation anglo-saxonne :
>>> 1.__class__
File "<stdin>", line 1
1.__class__
^
SyntaxError: invalid decimal literal
Il faut séparer l'entier du point, soit avec une espace, soit avec des parenthèses.
>>> 1 .__class__
<class 'int'>
>>> (1).__class__
<class 'int'>
Comment convertir une chaîne de caractères en nombre ?¶
Pour les entiers, utilisez le constructeur natif de int()
, par exemple int('144') == 144
. De façon similaire, float()
donne la valeur flottante, par exemple float('144') == 144.0
.
Par défaut, ces fonctions interprètent les nombres comme des décimaux, de telle façon que int('0144') == 144
et int('0x144')
lève une ValueError
. Le second argument (optionnel) de int(string, base)
est la base dans laquelle convertir, donc int('0x144', 16) == 324
. Si la base donnée est 0, le nombre est interprété selon les règles Python : un préfixe 0o
indique de l'octal et 0x
indique de l'hexadécimal.
N'utilisez pas la fonction native eval()
pour convertir des chaînes de caractères en nombres. eval()
est beaucoup plus lente et pose des problèmes de sécurité : quelqu'un pourrait vous envoyer une expression Python pouvant avoir des effets de bord indésirables. Par exemple, quelqu'un pourrait passer __import__('os').system("rm -rf $HOME")
ce qui effacerait votre répertoire personnel.
eval()
a aussi pour effet d'interpréter les nombres comme des expressions Python. Ainsi eval('09')
produit une erreur de syntaxe, parce que Python ne permet pas les '0' en tête d'un nombre décimal (à l'exception du nombre '0').
Comment convertir un nombre en chaîne de caractères ?¶
Pour transformer, par exemple, le nombre 144
en la chaîne de caractères '144'
, il faut utiliser la fonction native str()
. Pour obtenir la représentation hexadécimale ou octale, il faut utiliser les fonctions natives hex()
ou oct()
. Pour des représentations non-conventionnelles, se référer aux sections f-strings et Syntaxe de formatage de chaîne, par exemple "{:04d}".format(144)
produit '0144'
et "{:.3f}".format(1.0/3.0)
produit '0.333'
.
Comment modifier une chaîne de caractères « sur place » ?¶
C'est impossible car les chaînes de caractères sont immuables. Dans la plupart des cas, il faut tout simplement construire une nouvelle chaîne à partir des morceaux de l'ancienne. Si toutefois vous avez besoin d'un objet capable de modifier de la donnée Unicode « sur place », essayez d'utiliser un objet io.StringIO
ou le module array
:
>>> import io
>>> s = "Hello, world"
>>> sio = io.StringIO(s)
>>> sio.getvalue()
'Hello, world'
>>> sio.seek(7)
7
>>> sio.write("there!")
6
>>> sio.getvalue()
'Hello, there!'
>>> import array
>>> a = array.array('u', s)
>>> print(a)
array('u', 'Hello, world')
>>> a[0] = 'y'
>>> print(a)
array('u', 'yello, world')
>>> a.tounicode()
'yello, world'
Comment utiliser des chaînes de caractères pour appeler des fonctions/méthodes ?¶
Il y a plusieurs façons de faire.
La meilleure est d'utiliser un dictionnaire qui fait correspondre les chaînes de caractères à des fonctions. Le principal avantage de cette technique est que les chaînes n'ont pas besoin d'être égales aux noms de fonctions. C'est aussi la façon principale d'imiter la construction "case" :
def a(): pass def b(): pass dispatch = {'go': a, 'stop': b} # Note lack of parens for funcs dispatch[get_input()]() # Note trailing parens to call function
Utiliser la fonction
getattr()
:import foo getattr(foo, 'bar')()
Notez que
getattr()
marche sur n'importe quel objet, ceci inclut les classes, les instances de classes, les modules et ainsi de suite.Ceci est utilisé à plusieurs reprises dans la bibliothèque standard, de cette façon :
class Foo: def do_foo(self): ... def do_bar(self): ... f = getattr(foo_instance, 'do_' + opname) f()
Utilisez
locals()
pour résoudre le nom de la fonction :def myFunc(): print("hello") fname = "myFunc" f = locals()[fname] f()
Existe-t-il un équivalent à la fonction chomp()
de Perl, pour retirer les caractères de fin de ligne d'une chaîne de caractères ?¶
Vous pouvez utiliser S.rstrip("\r\n")
pour retirer toutes les occurrences de tout marqueur de fin de ligne à la fin d'une chaîne de caractère S
, sans en enlever aucune espace. Si la chaîne S
représente plus d'une ligne, avec plusieurs lignes vides, les marqueurs de fin de ligne de chaque ligne vide seront retirés :
>>> lines = ("line 1 \r\n"
... "\r\n"
... "\r\n")
>>> lines.rstrip("\n\r")
'line 1 '
Vu que cela ne sert presque qu'à lire un texte ligne à ligne, utiliser S.rstrip()
de cette manière fonctionne correctement.
Existe-t-il un équivalent à scanf()
ou sscanf()
?¶
Pas exactement.
Pour une simple analyse de chaîne, l'approche la plus simple est généralement de découper la ligne en mots délimités par des espaces, en utilisant la méthode split()
des objets chaîne de caractères, et ensuite de convertir les chaînes de décimaux en valeurs numériques en utilisant la fonction int()
ou float()
. split()
possède un paramètre optionnel "sep" qui est utile si la ligne utilise autre chose que des espaces comme séparateurs.
Pour des analyses plus compliquées, les expressions rationnelles sont plus puissantes que la fonction sscanf
de C et mieux adaptées à la tâche.
Que signifient les erreurs UnicodeDecodeError
ou UnicodeEncodeError
?¶
Voir Guide Unicode.
Can I end a raw string with an odd number of backslashes?¶
A raw string ending with an odd number of backslashes will escape the string's quote:
>>> r'C:\this\will\not\work\'
File "<stdin>", line 1
r'C:\this\will\not\work\'
^
SyntaxError: unterminated string literal (detected at line 1)
There are several workarounds for this. One is to use regular strings and double the backslashes:
>>> 'C:\\this\\will\\work\\'
'C:\\this\\will\\work\\'
Another is to concatenate a regular string containing an escaped backslash to the raw string:
>>> r'C:\this\will\work' '\\'
'C:\\this\\will\\work\\'
It is also possible to use os.path.join()
to append a backslash on Windows:
>>> os.path.join(r'C:\this\will\work', '')
'C:\\this\\will\\work\\'
Note that while a backslash will "escape" a quote for the purposes of determining where the raw string ends, no escaping occurs when interpreting the value of the raw string. That is, the backslash remains present in the value of the raw string:
>>> r'backslash\'preserved'
"backslash\\'preserved"
Also see the specification in the language reference.
Performances¶
Mon programme est trop lent. Comment l'accélérer ?¶
Question difficile en général. Il faut garder en tête les points suivants avant d'aller plus loin :
Les performances varient en fonction des implémentations de Python. Cette FAQ ne traite que de CPython.
Les comportements peuvent différer d'un système d'exploitation à l'autre, tout particulièrement quand il s'agit d'entrée/sortie ou de fils d'exécution multiples.
Il faut toujours essayer de trouver où sont les points de contention d'un programme avant d'essayer d'optimiser du code (voir le module
profile
).Écrire des scripts d'évaluation de performances permet de progresser rapidement dans la recherche d'améliorations (voir le module
timeit
).Il est très fortement recommandé d'avoir une bonne couverture de code (avec des tests unitaires ou autre) avant d'ajouter des erreurs dans des optimisations sophistiquées.
Ceci étant dit, il y a beaucoup d'astuces pour accélérer du code Python. Voici quelques principes généraux qui peuvent aider à atteindre des niveaux de performance satisfaisants :
Améliorer les algorithmes (ou en changer pour des plus performants) peut produire de bien meilleurs résultats que d'optimiser çà et là de petites portions du code.
Utiliser les structures de données adaptées. Se référer à la documentation des Types natifs et du module
collections
.Quand la bibliothèque standard fournit une implémentation pour quelque chose, il y a de fortes chances (même si ce n'est pas systématique) que cette implémentation soit plus rapide que la vôtre. C'est d'autant plus vrai pour les routines écrites en C, comme les routines natives et certaines extensions de types. Par exemple, il faut utiliser la méthode native
list.sort()
ou la fonctionsorted()
similaire pour classer (et se référer à la section Guide pour le tri pour des exemples d'utilisation courante).Les abstractions ont tendance à créer des indirections et obligent l'interpréteur à faire plus d'efforts. Si le niveau d'indirection dépasse la quantité de travail effectif, le programme sera ralenti. Il faut toujours éviter trop d'indirections, en particulier sous la forme de fonctions ou méthodes trop petites (qui nuisent aussi souvent à la clarté du code).
Si vous atteignez les limites de ce que du Python « pur » permet de faire, il y a des outils qui permettent d'aller plus loin. Par exemple, Cython peut compiler une version légèrement modifiée de code Python en une extension C et est disponible sur de nombreuses plate-formes. Cython peut bénéficier de la compilation (et de l'annotation, optionnelle, des types) pour rendre votre code beaucoup plus rapide que s'il était interprété. Si vous avez confiance en vos capacités de programmation en C, vous pouvez aussi écrire un module d'extension en C vous-même.
Voir aussi
La page wiki dédiée aux astuces de performance.
Quelle est la manière la plus efficace de concaténer un grand nombre de chaînes de caractères ?¶
Les objets str
et bytes
sont immuables, par conséquent concaténer un grand nombre de chaînes de caractères entre elles n'est pas très efficace car chaque concaténation crée un nouvel objet. Dans le cas général, la complexité est quadratique par rapport à la taille totale de la chaîne.
Pour mettre bout à bout un grand nombre d'objets str
, la technique recommandée consiste à toutes les mettre dans une liste et appeler la méthode str.join()
à la fin :
chunks = []
for s in my_strings:
chunks.append(s)
result = ''.join(chunks)
(une autre technique relativement efficace consiste à utiliser io.StringIO
)
Pour concaténer un grand nombre d'objets bytes
, la technique recommandée consiste à étendre un objet bytearray
en utilisant la concaténation en-place (l'opérateur +=
) :
result = bytearray()
for b in my_bytes_objects:
result += b
Séquences (n-uplets / listes)¶
Comment convertir les listes en n-uplets et inversement ?¶
Le constructeur de type tuple(seq)
convertit toute séquence (plus précisément, tout itérable) en un n-uplet avec les mêmes éléments dans le même ordre.
Par exemple tuple([1, 2, 3])
renvoie (1, 2, 3)
et tuple('abc')
renvoie ('a', 'b', 'c')
. Si l'argument est un n-uplet, cela ne crée pas de copie, mais renvoie le même objet, ce qui fait de tuple()
une fonction économique à appeler quand vous ne savez pas si votre objet est déjà un n-uplet.
Le constructeur de type list(seq)
convertit toute séquence ou itérable en liste contenant les mêmes éléments dans le même ordre. Par exemple, list((1,2,3))
renvoie [1,2,3]
et list('abc')
renvoie ['a','b','c']
. Si l'argument est une liste, il renvoie une copie, de la même façon que seq[:]
.
Qu'est-ce qu'un indice négatif ?¶
Les séquences Python sont indicées avec des nombres positifs aussi bien que négatifs. Pour les nombres positifs, 0 est le premier indice, 1 est le deuxième, et ainsi de suite. Pour les indices négatifs, -1
est le dernier indice, -2
est le pénultième (avant-dernier), et ainsi de suite. On peut aussi dire que seq[-n]
est équivalent à seq[len(seq)-n]
.
Utiliser des indices négatifs peut être très pratique. Par exemple S[:-1]
représente la chaîne tout entière à l'exception du dernier caractère, ce qui est pratique pour retirer un caractère de fin de ligne à la fin d'une chaîne.
Comment itérer à rebours sur une séquence ?¶
Utilisez la fonction native reversed()
:
for x in reversed(sequence):
... # do something with x ...
Cela ne modifie pas la séquence initiale, mais construit à la place une copie en ordre inverse pour itérer dessus.
Comment retirer les doublons d'une liste ?¶
Lisez le « livre de recettes » Python pour trouver une longue discussion sur les nombreuses approches possibles :
Si changer l'ordre de la liste ne vous dérange pas, commencez par ordonner celle-ci, puis parcourez-la d'un bout à l'autre, en supprimant les doublons trouvés en chemin :
if mylist:
mylist.sort()
last = mylist[-1]
for i in range(len(mylist)-2, -1, -1):
if last == mylist[i]:
del mylist[i]
else:
last = mylist[i]
Si tous les éléments de la liste peuvent être utilisés comme des clés de dictionnaire (c'est-à-dire, qu'elles sont toutes hachables) ceci est souvent plus rapide
mylist = list(set(mylist))
Ceci convertit la liste en un ensemble, ce qui supprime automatiquement les doublons, puis la transforme à nouveau en liste.
Comment retirer les doublons d'une liste¶
Comme pour supprimer les doublons, il est possible d’itérer explicitement à l’envers avec une condition de suppression. Cependant, il est plus facile et plus rapide d’utiliser le remplacement des tranches par une itération avant, implicite ou explicite. Voici trois variantes :
mylist[:] = filter(keep_function, mylist)
mylist[:] = (x for x in mylist if keep_condition)
mylist[:] = [x for x in mylist if keep_condition]
La liste en compréhension est peut-être la plus rapide :
Comment construire un tableau en Python ?¶
Utilisez une liste :
["this", 1, "is", "an", "array"]
Les listes ont un coût équivalent à celui des tableaux C ou Pascal ; la principale différence est qu'une liste Python peut contenir des objets de différents types.
Le module array
fournit des méthodes pour créer des tableaux de types fixes dans une représentation compacte, mais ils sont plus lents à indexer que les listes. Notez aussi que NumPy (et d'autres) fournissent différentes structures de type tableaux, avec des caractéristiques différentes.
Pour obtenir des listes chaînées à la sauce Lisp, vous pouvez émuler les cons cells en utilisant des n-uplets :
lisp_list = ("like", ("this", ("example", None) ) )
Si vous voulez pouvoir modifier les éléments, utilisez une liste plutôt qu'un n-uplet. Ici la version équivalente du car de Lisp est lisp_list[0]
et l'équivalent de cdr est lisp_list[1]
. Ne faites ceci que si vous êtes réellement sûr d'en avoir besoin, cette méthode est en général bien plus lente que les listes Python.
Comment créer une liste à plusieurs dimensions ?¶
Vous avez probablement essayé de créer une liste à plusieurs dimensions de cette façon :
>>> A = [[None] * 2] * 3
Elle semble correcte si on l'affiche :
>>> A
[[None, None], [None, None], [None, None]]
Mais quand vous affectez une valeur, celle-ci apparaît à plusieurs endroits :
>>> A[0][0] = 5
>>> A
[[5, None], [5, None], [5, None]]
Dupliquer une liste en utilisant *
ne crée en réalité pas de copie mais seulement des références aux objets existants. Le *3
crée une liste contenant trois références à la même liste de longueur deux. Un changement dans une colonne apparaîtra donc dans toutes les colonnes, ce qui n'est très probablement pas ce que vous souhaitiez.
L'approche suggérée est d'abord de créer une liste de la longueur désirée, puis de remplir tous les éléments avec une nouvelle chaîne :
A = [None] * 3
for i in range(3):
A[i] = [None] * 2
Cela génère une liste contenant elle-même trois listes distinctes, de longueur deux. Vous pouvez aussi utiliser la syntaxe des listes en compréhension :
w, h = 2, 3
A = [[None] * w for i in range(h)]
Vous pouvez aussi utiliser une extension qui fournit un type matriciel natif ; NumPy est la plus répandue.
Comment appliquer une méthode ou une fonction à une séquence d'objets ?¶
Pour appeler une méthode ou une fonction et accumuler les valeurs renvoyées dans une liste, une compréhension de liste est une solution élégante :
result = [obj.method() for obj in mylist]
result = [function(obj) for obj in mylist]
To just run the method or function without saving the return values,
a plain for
loop will suffice:
for obj in mylist:
obj.method()
for obj in mylist:
function(obj)
Pourquoi a_tuple[i] += ['item']
lève-t-il une exception alors que l'addition fonctionne ?¶
Ceci est dû à la combinaison de deux facteurs : le fait que les opérateurs d'affectation incrémentaux sont des opérateurs d'affectation et à la différence entre les objets mutables et immuables en Python.
Cette discussion est valable, en général, quand des opérateurs d'affectation incrémentale sont appliqués aux éléments d'un n-uplet qui pointe sur des objets mutables, mais on prendra list
et +=
comme exemple.
Si vous écrivez :
>>> a_tuple = (1, 2)
>>> a_tuple[0] += 1
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
La cause de l'exception est claire : 1
est ajouté à l'objet a_tuple[0]
qui pointe sur (1
), ce qui produit l'objet résultant 2
, mais, lorsque l'on tente d'affecter le résultat du calcul, 2
, à l'élément 0
du n-uplet, on obtient une erreur car il est impossible de modifier la cible sur laquelle pointe un élément d'un n-uplet.
Sous le capot, une instruction d'affectation incrémentale fait à peu près ceci :
>>> result = a_tuple[0] + 1
>>> a_tuple[0] = result
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
C'est la partie de l'affectation de l'opération qui génère l'erreur, vu qu'un n-uplet est immuable.
Quand vous écrivez un code du style :
>>> a_tuple = (['foo'], 'bar')
>>> a_tuple[0] += ['item']
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
L'exception est un peu plus surprenante et, chose encore plus étrange, malgré l'erreur, l'ajout a fonctionné :
>>> a_tuple[0]
['foo', 'item']
To see why this happens, you need to know that (a) if an object implements an
__iadd__()
magic method, it gets called when the +=
augmented
assignment
is executed, and its return value is what gets used in the assignment statement;
and (b) for lists, __iadd__()
is equivalent to calling extend()
on the list
and returning the list. That's why we say that for lists, +=
is a
"shorthand" for list.extend()
:
>>> a_list = []
>>> a_list += [1]
>>> a_list
[1]
C’est équivalent à :
>>> result = a_list.__iadd__([1])
>>> a_list = result
L'objet sur lequel pointe a_list
a été modifié et le pointeur vers l'objet modifié est réaffecté à a_list
. In fine, l'affectation ne change rien, puisque c'est un pointeur vers le même objet que sur lequel pointait a_list
, mais l'affectation a tout de même lieu.
Donc, dans notre exemple avec un n-uplet, il se passe quelque chose équivalent à :
>>> result = a_tuple[0].__iadd__(['item'])
>>> a_tuple[0] = result
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
L'appel à __iadd__()
réussit et la liste est étendue, mais bien que result
pointe sur le même objet que a_tuple[0]
, l'affectation finale échoue car les n-uplets ne sont pas mutables.
Je souhaite faire un classement compliqué : peut-on faire une transformation de Schwartz en Python ?¶
Cette technique, attribuée à Randal Schwartz de la communauté Perl, ordonne les éléments d'une liste à l'aide une transformation qui fait correspondre chaque élément à sa "valeur de tri". En Python, ceci est géré par l'argument key
de la méthode list.sort()
:
Isorted = L[:]
Isorted.sort(key=lambda s: int(s[10:15]))
Comment ordonner une liste en fonction des valeurs d'une autre liste ?¶
Fusionnez-les dans un itérateur de n-uplets, ordonnez la liste obtenue, puis choisissez l'élément que vous voulez
>>> list1 = ["what", "I'm", "sorting", "by"]
>>> list2 = ["something", "else", "to", "sort"]
>>> pairs = zip(list1, list2)
>>> pairs = sorted(pairs)
>>> pairs
[("I'm", 'else'), ('by', 'sort'), ('sorting', 'to'), ('what', 'something')]
>>> result = [x[1] for x in pairs]
>>> result
['else', 'sort', 'to', 'something']
Objets¶
Qu'est-ce qu'une classe ?¶
Une classe est le type d'objet particulier créé par l'exécution d'une déclaration de classe. Les objets de classe sont utilisés comme modèles pour créer des objets, qui incarnent à la fois les données (attributs) et le code (méthodes) spécifiques à un type de données.
Une classe peut être fondée sur une ou plusieurs autres classes, appelée sa (ou ses) classe(s) de base. Elle hérite alors des attributs et des méthodes de ses classes mères. Cela permet à un modèle d'objet d'être successivement raffiné par héritage. Vous pourriez avoir une classe générique Mailbox
, qui fournit des méthodes d'accès de base pour une boîte aux lettres, et des sous-classes telles que MboxMailbox
, MaildirMailbox
, OutlookMailbox
qui gèrent les plusieurs formats spécifiques de boîtes aux lettres.
Qu'est-ce qu'une méthode ?¶
Une méthode est une fonction sur un objet x
qu'on appelle de manière générale sous la forme x.name(arguments…)
. Les méthodes sont définies comme des fonctions à l'intérieur de la définition de classe :
class C:
def meth(self, arg):
return arg * 2 + self.attribute
Qu'est-ce que self ?¶
Par convention, le premier argument d'une méthode est appelé self. Une méthode meth(self, a, b, c)
doit être appelée sous la forme x.meth(a, b, c)
où x
est une instance de la classe dans laquelle cette méthode est définie ; tout se passe comme si la méthode était appelée comme meth(x, a, b, c)
.
Voir aussi Pourquoi self doit-il être explicitement utilisé dans les définitions et les appels de méthodes ?.
Comment vérifier si un objet est une instance d'une classe donnée ou d'une sous-classe de celle-ci ?¶
Utilisez la fonction native isinstance(obj, cls)
. Vous pouvez vérifier qu'un objet est une instance de plusieurs classes à la fois en fournissant un n-uplet à la place d'une seule classe, par exemple, isinstance(obj, (class1, class2, ...))
. Vous pouvez également vérifier qu'un objet est l'un des types natifs de Python, par exemple isinstance(obj, str)
ou isinstance(obj, (int, float, complex))
.
Notez que isinstance()
prend aussi en compte l'héritage virtuel d'une classe mère abstraite, c'est à dire qu'elle renvoie True
pour une classe A enregistrée auprès d'une classe mère abstraite B même si A n'est pas directement ou indirectement une classe fille de B. Pour vérifier l'héritage dans le sens plus restreint, parcourez l'ordre de résolution des méthodes de la classe :
from collections.abc import Mapping
class P:
pass
class C(P):
pass
Mapping.register(P)
>>> c = C()
>>> isinstance(c, C) # direct
True
>>> isinstance(c, P) # indirect
True
>>> isinstance(c, Mapping) # virtual
True
# Actual inheritance chain
>>> type(c).__mro__
(<class 'C'>, <class 'P'>, <class 'object'>)
# Test for "true inheritance"
>>> Mapping in type(c).__mro__
False
Notez que la plupart des programmes n'utilisent que rarement isInstance()
sur les classes définies par l'utilisateur. Si vous développez vous-même des classes, une approche plus orientée-objet consiste définir des méthodes sur les classes qui sont porteuses d'un comportement particulier, plutôt que de vérifier la classe de l'objet et de faire un traitement ad-hoc. Par exemple, si vous avez une fonction qui fait quelque chose :
def search(obj):
if isinstance(obj, Mailbox):
... # code to search a mailbox
elif isinstance(obj, Document):
... # code to search a document
elif ...
Une meilleure approche est de définir une méthode search()
dans toutes les classes et qu'il suffit d'appeler de la manière suivante :
class Mailbox:
def search(self):
... # code to search a mailbox
class Document:
def search(self):
... # code to search a document
obj.search()
Qu'est-ce que la délégation ?¶
La délégation est une technique orientée objet (aussi appelée « patron de conception). Prenons un objet x
dont on souhaite modifier le comportement d'une seule de ses méthodes. On peut créer une nouvelle classe qui fournit une nouvelle implémentation de la méthode qui nous intéresse dans l'évolution et qui délègue toute autre méthode à la méthode correspondante de x
.
Les programmeurs Python peuvent facilement mettre en œuvre la délégation. Par exemple, la classe suivante implémente une classe qui se comporte comme un fichier, mais convertit toutes les données écrites en majuscules :
class UpperOut:
def __init__(self, outfile):
self._outfile = outfile
def write(self, s):
self._outfile.write(s.upper())
def __getattr__(self, name):
return getattr(self._outfile, name)
Here the UpperOut
class redefines the write()
method to convert the
argument string to uppercase before calling the underlying
self._outfile.write()
method. All other methods are delegated to the
underlying self._outfile
object. The delegation is accomplished via the
__getattr__()
method; consult the language reference
for more information about controlling attribute access.
Note that for more general cases delegation can get trickier. When attributes
must be set as well as retrieved, the class must define a __setattr__()
method too, and it must do so carefully. The basic implementation of
__setattr__()
is roughly equivalent to the following:
class X:
...
def __setattr__(self, name, value):
self.__dict__[name] = value
...
Most __setattr__()
implementations must modify
self.__dict__
to store
local state for self without causing an infinite recursion.
Comment appeler une méthode définie dans une classe mère depuis une classe dérivée qui la surcharge ?¶
Utilisez la fonction native super()
:
class Derived(Base):
def meth(self):
super().meth() # calls Base.meth
Dans cet exemple, super()
détermine automatiquement l'instance dont une méthode l'appelle (l'objet self
), observe l'ordre de résolution des méthodes obtenu avec type(self).__mro__
, et renvoie la classe qui suit Derived
dans cet ordre, donc Base
.
Comment organiser un code pour permettre de changer la classe mère plus facilement ?¶
Vous pouvez définir un alias pour la classe mère et dériver depuis l'alias. Ensuite, tout ce que vous devez changer est la valeur attribuée à cet alias. Accessoirement, cette astuce est également utile pour déterminer dynamiquement (par exemple en fonction de la disponibilité de certaines ressources) la classe mère à utiliser. Exemple :
class Base:
...
BaseAlias = Base
class Derived(BaseAlias):
...
Comment créer des données statiques de classe et des méthodes statiques de classe ?¶
Les données statiques et les méthodes statiques (au sens C++ ou Java) sont prises en charge en Python.
Pour les données statiques, il suffit de définir un attribut de classe. Pour attribuer une nouvelle valeur à l'attribut, vous devez explicitement utiliser le nom de classe dans l'affectation :
class C:
count = 0 # number of times C.__init__ called
def __init__(self):
C.count = C.count + 1
def getcount(self):
return C.count # or return self.count
c.count
se réfère également à C.count
pour tout c
tel que isInstance (c, C)
est vrai, sauf remplacement par c
lui-même ou par une classe sur le chemin de recherche de classe mère de c.__class__
jusqu'à C
.
Attention : dans une méthode de C, une affectation comme self.count = 42
crée une nouvelle instance sans rapport avec le nom count
dans le dictionnaire de données de self
. La redéfinition d'une donnée statique de classe doit toujours spécifier la classe, que l'on soit à l'intérieur d'une méthode ou non :
C.count = 314
Il est possible d'utiliser des méthodes statiques :
class C:
@staticmethod
def static(arg1, arg2, arg3):
# No 'self' parameter!
...
Cependant, d'une manière beaucoup plus simple pour obtenir l'effet d'une méthode statique se fait par une simple fonction au niveau du module :
def getcount():
return C.count
Si votre code est structuré de manière à définir une classe (ou bien la hiérarchie des classes connexes) par module, ceci fournira l'encapsulation souhaitée.
Comment surcharger les constructeurs (ou méthodes) en Python ?¶
Cette réponse s'applique en fait à toutes les méthodes, mais la question se pose généralement dans le contexte des constructeurs.
En C++, on écrirait
class C {
C() { cout << "No arguments\n"; }
C(int i) { cout << "Argument is " << i << "\n"; }
}
En Python, vous devez écrire un constructeur unique qui considère tous les cas en utilisant des arguments par défaut. Par exemple :
class C:
def __init__(self, i=None):
if i is None:
print("No arguments")
else:
print("Argument is", i)
Ce n'est pas tout à fait équivalent, mais suffisamment proche dans la pratique.
Vous pouvez aussi utiliser une liste d'arguments de longueur variable, par exemple
def __init__(self, *args):
...
La même approche fonctionne pour toutes les définitions de méthode.
J'essaie d'utiliser __spam
et j'obtiens une erreur à propos de _SomeClassName__spam
.¶
Les noms de variables commençant avec deux tirets bas sont « déformés », c'est un moyen simple mais efficace de définir des variables privées à une classe. Tout identifiant de la forme __spam
(commençant par au moins deux tirets bas et se terminant par au plus un tiret bas) est textuellement remplacé par _classname__spam
, où classname
est le nom de la classe en cours sans les éventuels tirets bas du début.
Cela ne garantit aucune protection : un utilisateur extérieur peut encore délibérément accéder à l'attribut _classname__spam
et les valeurs privées sont visibles dans l'objet __dict__
. De nombreux programmeurs Python ne prennent jamais la peine d'utiliser des noms de variable privés.
Ma classe définit __del__
mais elle n'est pas appelée lorsque je supprime l'objet.¶
Il y a plusieurs explications possibles.
The del
statement does not necessarily call __del__()
-- it simply
decrements the object's reference count, and if this reaches zero
__del__()
is called.
If your data structures contain circular links (e.g. a tree where each child has
a parent reference and each parent has a list of children) the reference counts
will never go back to zero. Once in a while Python runs an algorithm to detect
such cycles, but the garbage collector might run some time after the last
reference to your data structure vanishes, so your __del__()
method may be
called at an inconvenient and random time. This is inconvenient if you're trying
to reproduce a problem. Worse, the order in which object's __del__()
methods are executed is arbitrary. You can run gc.collect()
to force a
collection, but there are pathological cases where objects will never be
collected.
Despite the cycle collector, it's still a good idea to define an explicit
close()
method on objects to be called whenever you're done with them. The
close()
method can then remove attributes that refer to subobjects. Don't
call __del__()
directly -- __del__()
should call close()
and
close()
should make sure that it can be called more than once for the same
object.
Une alternative pour éviter les références cycliques consiste à utiliser le module weakref
, qui permet de faire référence à des objets sans incrémenter leur compteur de références. Par exemple, les structures d'arbres devraient utiliser des références faibles entre pères et fils (si nécessaire !).
Finally, if your __del__()
method raises an exception, a warning message
is printed to sys.stderr
.
Comment obtenir toutes les instances d'une classe ?¶
Python ne tient pas de registre de toutes les instances d'une classe (ni de n'importe quel type natif). Il est cependant possible de programmer le constructeur de la classe de façon à tenir un tel registre, en maintenant une liste de références faibles vers chaque instance.
Pourquoi le résultat de id()
peut-il être le même pour deux objets différents ?¶
La fonction native id()
renvoie un entier dont l'unicité est garantie durant toute la vie de l'objet. Vu qu'en CPython cet entier est en réalité l'adresse mémoire de l'objet, il est fréquent qu'un nouvel objet soit alloué à une adresse mémoire identique à celle d'un objet venant d'être supprimé. Comme l'illustre le code suivant :
>>> id(1000)
13901272
>>> id(2000)
13901272
Les deux identifiants appartiennent à des objets entiers créés juste avant l'appel à id()
et détruits immédiatement après. Pour s'assurer que les objets dont on veut examiner les identifiants sont toujours en vie, créons une nouvelle référence à l'objet :
>>> a = 1000; b = 2000
>>> id(a)
13901272
>>> id(b)
13891296
Quand puis-je raisonnablement utiliser le test d'identité is ?¶
L'opérateur is
détermine si deux objets sont identiques, c'est-à-dire le même objet. Le test a is b
est équivalent à id(a) == id(b)
.
La propriété la plus importante du test d'identité est qu'un objet est toujours identique à lui-même. Quelle que soit la valeur de a, a is a
vaut toujours True
. Un test d'identité est généralement plus rapide qu'un test d'égalité. De plus, contrairement à l'opérateur ==
, l'opérateur is
renvoie toujours un booléen, True
ou False
.
Cependant, les tests d'identité ne peuvent remplacer les tests d'égalité que si l'identité est garantie. C'est le cas dans les trois situations suivantes :
1) Assignments create new names but do not change object identity. After the
assignment new = old
, it is guaranteed that new is old
.
Le stockage d'un objet dans un conteneur qui fonctionne avec des références n'altère pas l'identité. Si s
est une liste, alors après l'affectation de x à l'indice 0, s[0] = x
, le test s[0] is x
s'évalue forcément à True
.
3) If an object is a singleton, it means that only one instance of that object
can exist. After the assignments a = None
and b = None
, it is
guaranteed that a is b
because None
is a singleton.
Dans la plupart des autres cas, un test d'identité n'est pas approprié par rapport à un test d'égalité. En particulier, il ne faut pas utiliser is
pour comparer à des constantes comme les entiers (type int
) ou des chaînes de caractères (type str
) car ces valeurs ne sont pas nécessairement des singletons :
>>> a = 1000
>>> b = 500
>>> c = b + 500
>>> a is c
False
>>> a = 'Python'
>>> b = 'Py'
>>> c = b + 'thon'
>>> a is c
False
De même, deux instances fraîchement créées d'un type de conteneurs mutables ne sont jamais identiques :
>>> a = []
>>> b = []
>>> a is b
False
Voici quelques exemples de la bibliothèque standard qui illustrent comment utiliser correctement les tests d'identité dans certaines situations particulières :
1) As recommended by PEP 8, an identity test is the preferred way to check
for None
. This reads like plain English in code and avoids confusion with
other objects that may have boolean values that evaluate to false.
2) Detecting optional arguments can be tricky when None
is a valid input
value. In those situations, you can create a singleton sentinel object
guaranteed to be distinct from other objects. For example, here is how
to implement a method that behaves like dict.pop()
:
_sentinel = object()
def pop(self, key, default=_sentinel):
if key in self:
value = self[key]
del self[key]
return value
if default is _sentinel:
raise KeyError(key)
return default
3) Container implementations sometimes need to augment equality tests with
identity tests. This prevents the code from being confused by objects such as
float('NaN')
that are not equal to themselves.
For example, here is the implementation of
collections.abc.Sequence.__contains__()
:
def __contains__(self, value):
for v in self:
if v is value or v == value:
return True
return False
Comment définir dans une classe fille les attributs d'une instance immuable ?¶
When subclassing an immutable type, override the __new__()
method
instead of the __init__()
method. The latter only runs after an
instance is created, which is too late to alter data in an immutable
instance.
Toutes les classes d'objets immuables suivantes ont des signatures de constructeur différentes de leur classe mère :
from datetime import date
class FirstOfMonthDate(date):
"Always choose the first day of the month"
def __new__(cls, year, month, day):
return super().__new__(cls, year, month, 1)
class NamedInt(int):
"Allow text names for some numbers"
xlat = {'zero': 0, 'one': 1, 'ten': 10}
def __new__(cls, value):
value = cls.xlat.get(value, value)
return super().__new__(cls, value)
class TitleStr(str):
"Convert str to name suitable for a URL path"
def __new__(cls, s):
s = s.lower().replace(' ', '-')
s = ''.join([c for c in s if c.isalnum() or c == '-'])
return super().__new__(cls, s)
Ces classes s'utilisent comme ceci :
>>> FirstOfMonthDate(2012, 2, 14)
FirstOfMonthDate(2012, 2, 1)
>>> NamedInt('ten')
10
>>> NamedInt(20)
20
>>> TitleStr('Blog: Why Python Rocks')
'blog-why-python-rocks'
Comment mettre en cache le résultat d'une méthode ?¶
Il existe deux outils principaux pour mettre en cache la valeur de retour d'une méthode, à savoir functools.cached_property()
, qui stocke les valeurs au niveau de l'instance, et functools.lru_cache()
, qui le fait au niveau de la classe.
La fonction cached_property
n'est applicable qu'aux méthodes sans argument. Elle n'induit aucune référence vers l'instance. Le cache est simplement conservé aussi longtemps que l'instance elle-même.
L'avantage est que le cache est supprimé aussitôt que l'instance est détruite. L'inconvénient est que les caches peuvent s'accumuler avec les instances, sans limite de nombre.
La fonction lru_cache
s'applique quant à elle aux méthodes dont les arguments sont hachables. Elle crée une référence forte à l'instance, sauf à passer par des circonvolutions pour que la référence soit faible.
L'avantage de l'algorithme LRU est d'offrir une limitation sur la taille du cache, que l'on peut régler avec le paramètre maxsize (NdT : LRU signifie Least Recently Used en anglais, les entrées du cache les plus anciennes sont évincées pour conserver la taille). L'inconvénient est qu'une référence forte vers l'instance est conservée dans le cache, ce qui maintient l'instance hors de portée du ramasse-miettes jusqu'à ce que l'entrée soit effacée du cache ou que le cache soit remis à zéro.
Voici une démonstration des différentes techniques :
class Weather:
"Lookup weather information on a government website"
def __init__(self, station_id):
self._station_id = station_id
# The _station_id is private and immutable
def current_temperature(self):
"Latest hourly observation"
# Do not cache this because old results
# can be out of date.
@cached_property
def location(self):
"Return the longitude/latitude coordinates of the station"
# Result only depends on the station_id
@lru_cache(maxsize=20)
def historic_rainfall(self, date, units='mm'):
"Rainfall on a given date"
# Depends on the station_id, date, and units.
Cet exemple suppose que l'attribut station_id n'est jamais changé. Si les attributs utilisés par la méthode sont susceptibles d'être modifiés, cached_property
ne peut pas fonctionner car elle ne peut pas détecter les mutations pour invalider le cache.
To make the lru_cache approach work when the station_id is mutable,
the class needs to define the __eq__()
and __hash__()
methods so that the cache can detect relevant attribute updates:
class Weather:
"Example with a mutable station identifier"
def __init__(self, station_id):
self.station_id = station_id
def change_station(self, station_id):
self.station_id = station_id
def __eq__(self, other):
return self.station_id == other.station_id
def __hash__(self):
return hash(self.station_id)
@lru_cache(maxsize=20)
def historic_rainfall(self, date, units='cm'):
'Rainfall on a given date'
# Depends on the station_id, date, and units.
Modules¶
Comment créer des fichiers .pyc
?¶
Quand un module est importé pour la première fois (ou si le fichier source a été modifié depuis la création du fichier compilé), un fichier .pyc
contenant le code précompilé est créé dans un sous-dossier __pycache__
du dossier contentant le fichier .py
. Le nom du fichier .pyc
est identique au fichier .py
et se termine par .pyc
, avec une partie centrale qui dépend du binaire python
qui l'a créé (voir la PEP 3147 pour de plus amples précisions).
Une des raisons pour lesquelles un fichier .pyc
peut ne pas être créé est un problème de droits sur le dossier qui contient le fichier source, ce qui veut dire qu'il est impossible de créer le sous-dossier __pycache__
. Ceci peut arriver, par exemple, si vous développez en tant qu'un certain utilisateur, mais que le code est exécuté en tant qu'un autre utilisateur, par exemple pour tester un serveur Web.
La création du fichier .pyc
est automatique durant l'importation d'un module si Python est capable (en termes de droits, d'espace disque, etc) de créer un sous-dossier __pycache__
et d'écrire le module ainsi compilé dans ce sous-répertoire, à moins que la variable d'environnement PYTHONDONTWRITEBYTECODE
soit définie.
Exécuter du Python dans un script de plus haut niveau n'est pas considéré comme une importation et le fichier .pyc
n'est pas créé. Par exemple, si un module de plus haut niveau foo.py
importe un autre module xyz.py
, alors à l'exécution de foo
(en tapant python foo.py
dans la console), un fichier .pyc
est créé pour xyz
mais pas pour foo
car foo.py
n'est pas importé.
Pour créer un fichier .pyc
pour foo
— c'est-à-dire créer un fichier .pyc
pour un module qui n'est pas importé — il existe les modules py_compile
et compileall
.
Le module py_compile
peut compiler n'importe quel module manuellement. Il est ainsi possible d'appeler la fonction compile()
de manière interactive :
>>> import py_compile
>>> py_compile.compile('foo.py')
Ces lignes écrivent le .pyc
dans un sous-dossier __pycache__
à côté de foo.py
(le paramètre optionnel cfile
permet de changer ce comportement).
Tous les fichiers d'un ou plusieurs dossiers peuvent aussi être compilés avec le module compileall
. C'est possible depuis l'invite de commande en exécutant compileall.py
avec le chemin du dossier contenant les fichiers Python à compiler :
python -m compileall .
Comment obtenir le nom du module actuel ?¶
Un module peut déterminer son propre nom en examinant la variable globale prédéfinie __name__
. Si celle-ci vaut '__main__'
, c'est que le programme est exécuté comme un script. Beaucoup de modules qui doivent normalement être importés pour pouvoir être utilisés fournissent aussi une interface en ligne de commande ou un test automatique. Ils n'exécutent cette portion du code qu'après avoir vérifié la valeur de __name__
:
def main():
print('Running test...')
...
if __name__ == '__main__':
main()
Comment avoir des modules qui s'importent mutuellement ?¶
Considérons les modules suivants :
foo.py
:
from bar import bar_var
foo_var = 1
bar.py
:
from foo import foo_var
bar_var = 2
Le problème réside dans les étapes que l'interpréteur va réaliser :
main importe foo ;
Les variables globales (vides) de foo sont créées ;
foo est compilé et commence à s'exécuter ;
foo importe bar ;
Les variables globales (vides) de bar sont créées ;
bar est compilé et commence à s'exécuter ;
bar importe foo (en réalité, rien ne passe car il y a déjà un module appelé foo) ;
La mécanique des importations tente de lire foo_var dans les variables globales de foo pour procéder à l'affectation
bar.foo_var = foo.foo_var
.
La dernière étape échoue car Python n'a pas fini d'interpréter foo
et le dictionnaire global des symboles de foo
est encore vide.
Le même phénomène arrive quand on utilise import foo
, et qu'on essaye ensuite d'accéder à foo.foo_var
dans le code global.
Il y a (au moins) trois façons de contourner ce problème.
Guido van Rossum déconseille d'utiliser from <module> import ...
et de mettre tout le code dans des fonctions. L'initialisation des variables globales et des variables de classe ne doit utiliser que des constantes ou des fonctions natives. Ceci implique que tout ce qui est fourni par un module soit référencé par <module>.<nom>
.
Jim Roskind recommande d'effectuer les étapes suivantes dans cet ordre dans chaque module :
les exportations (variables globales, fonctions et les classes qui ne nécessitent d'importer des classes mères)
les instructions
import
le code (avec les variables globales qui sont initialisées à partir de valeurs importées).
van Rossum désapprouve cette approche car les importations se trouvent à un endroit bizarre, mais cela fonctionne.
Matthias Urlichs conseille de restructurer le code pour éviter les importations récursives.
Ces solutions peuvent être combinées.
__import__('x.y.z')
renvoie <module 'x'>
; comment accéder à z
?¶
Utilisez plutôt la fonction import_module()
de importlib
:
z = importlib.import_module('x.y.z')
Quand j'édite un module et que je le réimporte, je ne vois pas les changements. Pourquoi ?¶
Pour des raisons de performance et de cohérence, Python ne lit le fichier d'un module que la première fois où celui-ci est importé. Si ce n'était pas le cas, dans un programme composé d'un très grand nombre de modules qui importent tous le même module de base, ce module de base serait analysé et ré-analysé un très grand nombre de fois. Pour forcer la relecture d'un module, il faut faire :
import importlib
import modname
importlib.reload(modname)
Attention, cette technique ne marche pas systématiquement. En particulier, les modules qui contiennent des instructions comme
from modname import some_objects
continuent de fonctionner avec l'ancienne version des objets importés. Si le module contient une définition de classe, les instances déjà existantes de celle-ci ne sont pas mises à jour avec la nouvelle définition de la classe. Ceci peut conduire au comportement paradoxal suivant :
>>> import importlib
>>> import cls
>>> c = cls.C() # Create an instance of C
>>> importlib.reload(cls)
<module 'cls' from 'cls.py'>
>>> isinstance(c, cls.C) # isinstance is false?!?
False
La nature du problème apparaît clairement en affichant « l'identité » des objets de la classe :
>>> hex(id(c.__class__))
'0x7352a0'
>>> hex(id(cls.C))
'0x4198d0'