Algo & Prog 1 Python : CM séance 05
5. Docstring, paramètres, copie et portée
5.1. Docstring
5.1.1. Documentation des fonctions
Chaque fonction peut être documentée par une chaîne de caractère :
def toto (foo, bar) :
"Ceci est la documentation de toto"
print ("J'ai reçu", foo, bar)
Utilisation dans la console :
>>> toto(1,2)
>>> help(toto)
On peut écrire la documentation sur plusieurs lignes, avec les chaînes
multi-lignes entre triples doubles-quotes """
ou simples-quotes '''
:
def toto (foo, bar) :
"""Ceci est la documentation de toto :
- le paramètre foo est obligatoire ;
- le paramètre bar l'est aussi.
"""
print ("""Les chaines multi-lignes
peuvent être employées partout
mais attention aux espaces à gauche...""")
>>> help(toto)
>>> toto(1,2)
Mais où est stocké cette docstring ?
>>> toto.__doc__
Lorsqu'il n'y a pas de docstring, __doc__
est None :
def titi () :
pass
>>> titi.__doc__ == None # True
On peut donc aussi s'en servir dans un programme :
>>> toto(1) # TypeError: toto() missing 1 required pos. argument: 'bar'
try :
toto(1)
except TypeError as detail :
print("Erreur:", detail)
if toto.__doc__ != None :
print("Voici le doctring de la fonction:\n\n", toto.__doc__)
On peut même changer le docstring :
toto.__doc__ = "Ceci est la nouvelle aide"
>>> help(toto)
5.1.2. Documentation des classes
Les docstrings sont très utiles également pour les classes et leurs méthodes.
Exemple :
class Hello :
"""
Classe permettant de saluer quelqu'un.
Usage :
h = Hello("World")
h.saluer()
"""
def __init__ (self, nom) :
"""
Paramètre : le nom de la personne à saluer.
"""
self.nom = nom
def saluer(self) :
"""
Affiche le message de salutation.
"""
print("Hello", self.nom, "!")
L'aide affichée sera :
>>> help(Hello)
Help on class Hello in module __main__:
class Hello(builtins.object)
| Hello(nom)
|
| Classe permettant de saluer quelqu'un.
| Usage :
| h = Hello("World")
| h.saluer()
|
| Methods defined here:
|
| __init__(self, nom)
| Paramètre : le nom de la personne à saluer.
|
| saluer(self)
| Affiche le message de salutation.
|
...
5.2. Paramètres optionnels
Jusqu'à présent, tous les paramètres des fonctions que l'on a écrites sont obligatoires.
5.2.1. Principe
On peut aussi donner des paramètres optionnels après les paramètres obligatoires :
def toto (foo, bar, ga=None, bu="", zo=666) :
print ("foo =", foo, "bar =", bar, "ga =", ga, "bu =", bu, "zo =", zo)
Il suffit de leur donner une valeur initiale.
Exemple :
>>> toto(1) # erreur manque argument
>>> toto(1,2) # foo = 1 bar = 2 ga = None bu = zo = 666
>>> toto(1,2,"do") # foo = 1 bar = 2 ga = do bu = zo = 666
>>> toto(1,2,"do","ré","mi") # foo = 1 bar = 2 ga = do bu = ré zo = mi
Plus fort : ces paramètres optionnels peuvent être donnés dans un ordre quelconque en précisant leur nom (toujours après les obligatoires) :
>>> toto(1, 2, bu="ré", ga="do") #foo = 1 bar = 2 ga = do bu = ré zo = 666
5.2.2. Expansion
On peut récupérer tous les arguments dans un tuple
avec *args
:
def calculer_somme(*args) :
s = 0
for x in args :
s += x
return s
>>> calculer_somme(5,8,6) # 19
Remarque :
c'est le *
qui donne le mécanisme, pas args
, on peut mettre un autre nom.
L'opérateur *
s'appelle l'opérateur d'expansion positionnelle (uk : unpack, splat, starred expression).
Enfin on peut récupérer tous les arguments dans un dict
avec **kwargs
(pour : keyworded args) avec l'opérateur **
d'expansion par mots clés :
def tutu (**kwargs) :
for key,val in kwargs.items() :
print("Reçu", key, ":", val)
>>> tutu (racine="carrée", longueur=1.4142, pi="po")
Reçu racine : carrée
Reçu longueur : 1.4142
Reçu pi : po
On peut utiliser les 4 mécanismes ensemble, mais dans cet ordre :
- obligatoires
*args
- optionnels nommés
**kwargs
Ces possibilités sont beaucoup employées dans TkInter :
zone_dessin = tkinter.Canvas(fen_princ, bg="white", width=xmax, height=ymax)
5.2.3. Arguments optionnels dans une classe
Les méthodes de classes étant des fonctions, on peut utiliser les arguments optionnels dans les méthodes, en particulier dans le constructeur :
class Point :
def __init__ (self, x=0, y=0) :
self.x = x
self.y = y
def __repr__(self) :
return "(" + str(self.x) + "," + str(self.y) + ")"
>>> Point() # (0,0)
>>> Point(2,3) # (2,3)
>>> Point(y=4) # (0,4)
5.2.4. Expansion de paramètres
On peut aussi utiliser les opérateurs d'expansion *
et **
pour le passage
de paramètres, avec des fonctions qui ne l'ont à priori pas prévu :
>>> l = [5,7,11]
>>> print(l)
[5, 7, 11]
>>> print(*l) # expansé en : print(5, 7, 11)
5 7 11
>>> print(*l, sep=" ; ") # expansé en : print(5, 7, 11, sep=" ; ")
5 ; 7 ; 11
>>> d = { "sep" : " ; ", "end" : " .\n"}
>>> print(*l, **d) # -> print(5, 7, 11, sep=" ; ", end=" .\n")
5 ; 7 ; 11 .
5.3. Références et copies
On a vu au cours n°1 que toutes les variables en Python sont des références vers des objets (autrement dit, chaque variable contient l'adresse d'un objet). Revenons sur les conséquences :
5.3.1. Copie légère ou profonde
Conséquence 1 : les affectations recopient la référence, pas la valeur :
>>> a = {"ga": ["bu", "zo"]}
>>> b = a
>>> b["meu"] = "shadoks"
>>> a # {'ga': ['bu', 'zo'], 'meu': 'shadoks'}
→ si on modifie b
cela modifie a
, et vice-versa.
On peut le voir aussi avec :
>>> id(a) # 139961393481736 id renvoie l'adresse de l'objet
>>> id(b) # 139961393481736
>>> b is a # True is compare les adresses
Pour éviter des effets de bord, c'est-à-dire des effets indésirables dans la suite d'un programme, on peut faire une copie de l'objet référencé :
-
Pour les
dict
:>>> b = a.copy() >>> id(b) # 139961393481800 >>> b is a # False
-
Pour les
list
:Autre méthode avec un slice :>>> c = [] >>> d = c.copy() >>> d is c # False
>>> e = c[:] >>> e is c # False
Mais ces copies sont en réalité superficielles (on parle de copie légère, uk : shallow copy), en effet elles ne font pas de copies du contenu des objets, mais de leur adresse :
>>> a = {"ga": ["bu", "zo"]}
>>> b = a.copy()
>>> b is a # False
>>> b['ga'] # ['bu', 'zo']
>>> b['ga'] is a['ga'] # True
Pour réaliser une copie profonde (uk : deep copy), qui crée récursivement
des copies du contenu on peut utiliser la fonction deepcopy
du module copy
:
>>> import copy
>>> help(copy)
>>> c = copy.deepcopy(a)
>>> c is a # False
>>> c['ga'] # ['bu', 'zo']
>>> c['ga'] is a['ga'] # False
Pour les classes que l'on crée, on peut également utiliser les fonctions
copy
et deepcopy
du module copy
:
>>> class Foo:
def __init__ (self, bar) :
self.bar = bar
>>> f = Foo([1,2,3])
>>> g = f
>>> g is f # True
>>> h = copy.copy(f) # -> copie légère (shallow copy)
>>> h is f # False
>>> h.bar is f.bar # True
>>> k = copy.deepcopy(f) # -> copie profonde (deep copy)
>>> k is f # False
>>> k.bar is f.bar # False
5.3.2. Passage par référence
Conséquence 2 : tous les passages de paramètres des fonctions se font par référence :
def toto0 (a, b, c) :
print (id(a), id(b), id(c))
>>> i = 1000 ; s = "hop" ; l = [2, 3]
>>> print (id(i), id(s), id(l))
140217608327376 140217608403464 140217608347016
>>> toto0 (i, s, l)
140217608327376 140217608403464 140217608347016
La fonction a bien reçu les même références en paramètre.
Les listes et les dictionnaires sont des conteneurs muables, donc une fonction peut modifier leur contenu :
def toto1 (malist, mondict) :
malist.append("do")
mondict["ga"] = "bu"
>>> l = [] ; d = {}
>>> toto1 (l, d)
>>> print (l, d) # ['do'] {'ga': 'bu'}
Par contre, les affectations font perdre la référence reçue en paramètre :
def toto2 (malist, mondict) :
malist = ["do"] # référence perdue -> objet local
mondict = {"ga" : "bu"} # référence perdue -> objet local
>>> l = [] ; d = {}
>>> toto2 (l, d)
>>> print (l, d) # [] {}
Pour les types immuables, on ne peut que faire une affectation, donc on perd également la référence reçue en paramètre :
def toto3 (montuple, entier) :
montuple = (3,4)
entier = 5
>>> t = () ; e = 1
>>> toto3 (t, e)
>>> print (t, e) # () 1
5.3.3. Références et entiers
Les entiers sont stockés sous forme d'objets (pour que l'affectation puisse
recopier une référence).
Dans l'implémentation actuelle de python3
ils prééxistent de -5 à 256 ;
les autres sont créés lorsque nécessaire.
>>> for i in range(-10,300) : print(i, id(i))
>>> a = 5
>>> a is 5 # True
>>> a = 500
>>> a is 500 # False ce 500 a été créé ici et a une autre adresse
>>> id(500) # 139961323766000
>>> id(500) # 139961323766000 même adresse car 1ère disponible
>>> a = 500
>>> id() # 139961323766000 même adresse car 1ère disponible
>>> id(500) # 139961323766096 autre adresse car précédente occupée
Comme les entiers sont des objets on peut examiner leurs propriétés
avec dir
:
>>> dir(-2)
>>> (-2).__abs__()
Le ramasse-miette de Python détruit automatiquement tout objet
non référencé, sauf les entiers de -5 à 256, None
et l'ellipse ...
5.4. Portée des variables
La portée (uk : scope) d'une variable est la partie d'un programme où elle est accessible :
-
une variable créée dans le programme principal est accessible dans tout le programme (portée globale) ;
-
une variable crée dans une fonction n'est accessible que dans la fonction (portée locale) et ses éventuelles sous-fonctions.
a = 1 # crée une variable de portée globale
def f() :
print(a)
f() # 1
def g() :
b = 1 # crée un variable de portée locale
g()
print(b) # NameError: name 'b' is not defined
Cette variable b
sera d'ailleurs détruite dès la sortie de g()
puisqu'on sort
de sa portée.
Plusieurs variables portant le même nom peuvent coexister lorsqu'elles ont des portées différentes ; la portée locale l'emporte :
c = 1 # globale
def h() :
c = 2 # locale
print(c)
h() # 2
print(c) # 1
Dans une fonction on peut préciser la portée globale d'une variable :
d = 1
def i() :
global d
d = 2
print(d)
i() # 2
print(d) # 2
On notera que la variable n'a pas besoin d'avoir été déjà créée pour être déclarée globale dans la fonction :
def j() :
global e
e = 3
print(e)
j() # 3
print(e) # 3