Aller au contenu

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 :

  1. obligatoires
  2. *args
  3. optionnels nommés
  4. **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 :

    >>> c = []
    >>> d = c.copy()
    >>> d is c          # False
    
    Autre méthode avec un slice :

    >>> 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