[PageD'Accueil] [IndexDesTitres] [IndexDesTermes

Référence ou Valeur

Une grande question qui revient régulièrement : est-ce que les paramètres sont passés par référence ou par valeurs, puis-je les modifier?

Le modèle de Python

En Python, TOUT est objet: les valeurs numériques, les chaînes de caractères, les listes tuples et dictionnaires, les fonctions, les classes, les instances de classes (ouf), les modules... tout. Les variables ne sont que des noms qui sont associés à des objets.

Lorsque vous écrivez a=1, vous créez un nom "a" qui référence un objet correspondant à la valeur entière 1, et lorsque vous écrivez b=a, vous créez un nom "b" qui référence le même objet que le nom a. Vous pouvez vérifier cela en utilisant la fonction id de Python qui retourne l'identificateur unique associé à l'objet (en fait actuellement son adresse en mémoire):

>>> a=1
>>> id(a)
134526808
>>> b=a
>>> id(b)
134526808

Lorsque vous affectez une valeur à un nom:

Note: pour gérer la durée de vie des objets, donc leur destruction lorsqu'ils ne sont plus utiles, C-Python utilise un système de compteur de références pour chaque objet ; chaque nouveau nom référençant un objet incrémente ce compteur, et chaque nom ne référençant plus un objet décrémente ce compteur... lorsque le compteur arrive à zéro, c'est que plus personne n'en a besoin, et l'objet est détruit.

Mutable et Immutable

Immutable

Lorsque l'on fait des opérations sur des objets immutables pour obtenir une nouvelle valeur, on est obligé de construire un autre objet car les opérations ne permettent pas de modifier l'original.

>>> s = "Bonjour"
>>> id(s)
134875344
>>> s += " a vous."
>>> id(s)
134859448

Ce qui correspond à s = s + " a vous.".

Les objets immutables sont les valeurs numériques, les chaînes de caractères, et les tuples.

L'intérêt des objets immutables est que l'on est sûr que personne ne pourra modifier leur valeur. Tous les noms référençant un objet immutable peuvent considérer qu'il s'agit d'une constante tant qu'ils continuent à le référencer.

Mutable

Les objets mutables offrent des opérations permettant de modifier la valeur contenue dans l'objet, en conservant le même objet.

>>> l = [ 1,2,3,4 ]
>>> id(l)
134865140
>>> l += [ 5,6,7,8 ]
>>> id(l)
134865140

Les objets mutables sont les listes, les dictionnaires. Généralement les autres objets (ceux que vous créez) sont aussi mutables.

Les noms qui référencent des objets mutables peuvent voir l'objet qu'ils référencent modifié (par une fonction appelée, par du code exécuté dans un autre thread...).

Passage de Paramètres

Donc, en Python tous les paramètres sont passés par référence (références à des objets), ces références étant associées aux noms des paramèters lors de l'appel.

>>> def f(x) :
...     print id(x)
...
>>> y="Bonjour"
>>> id(y)
134875344
>>> f(y)
134875344
>>> l = [ 1,2,3,4 ]
>>> id(l)
134872372
>>> f(l)
134872372

Mais!!!

Note: le passage d'une valeur mutable en paramètre peut être utilisée pour effectuer un retour de valeur (ex. on passe une liste vide, et la fonction la remplit), mais c'est à éviter autant que possible, il vaut mieux avoir un retour explicite d'une valeur plutôt que se baser sur des effets de bord plus ou moins cachés.

NB : D'autant qu'en Python, une fonction peut renvoyer plusieurs valeurs (en fait un tuple) :

>>> def fun(): 
>>> ...return 1, 2, 3
>>> ... 
>>> a, b, c = fun() 
>>> a, b, c 
(1, 2, 3) 

Portée et espaces de noms

En reprenant le modèle de Python décrit dans le chapitre précédent, on voit que Python fonctionne par des associations entre des noms et des objets. Ceci se retrouve à tous les niveaux, les modules, les fonctions et méthodes, les objets...

On a donc des conteneurs de noms, que l'on appelle espaces de noms, et des noms contenus dans ces conteneurs. La fonction dir permet de connaître les noms définis dans un espace de noms:

>>> dir()
['__builtins__', '__doc__', '__file__', '__name__']
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FloatingPointError', 'FutureWarning', 'IOError', 'ImportError', 'IndentationError', 'IndexError', 'KeyError', 
...
'sum', 'super', 'tuple', 'type', 'unichr', 'unicode', 'vars', 'xrange', 'zip']
>>> class Toto :
...     def __init__ (self) :
...             self.a = 3
...             self.b = 5
...     def Coucou (self) :
...             print "Coucou"
...
>>> dir(Toto)
['Coucou', '__doc__', '__init__', '__module__']
>>> t=Toto()
>>> dir(t)
['Coucou', '__doc__', '__init__', '__module__', 'a', 'b']
>>> dir (Toto.Coucou)
['__call__', '__class__', '__cmp__', '__delattr__', '__doc__', '__get__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', 'im_class', 'im_func', 'im_self']

Reste à voir de quelle façon Python parcourt les espaces de noms (résoud les noms en objets), et les pièges qui peuvent se cacher derrière ce fonctionnement (entre autres pour les programmeurs ayant débuté avec d'autres langages). Une bonne compréhension du fonctionnement des espaces de noms permet d'éviter ces pièges et de mieux appréhender la puissance de Python.

Niveaux d'espaces de noms

Les "builtins"

Les builtins (littéralement "fabriquées dedans") sont les noms définis de façon interne à l'interpréteur. Ils sont accessibles de partout sans avoir à importer quoi que ça soit. C'est dans cet espace de noms que l'on retrouve les fonctions, objets, constantes courament utilisés (ex. open, print, None...).

Les globales

Les variables globales d'un module X sont les noms définis dans ce module et accessibles à partir de celui-ci. Cela recouvre les globales directement définies dans X, et celles définies dans d'autres modules mais importées dans X. Il est important de noter qu'en Python aucune variable globale n'est complètement privée à un module, il est toujours possible d'y accéder de l'extérieur.

Définition de nom

Lorsque l'on définit un nom (que l'on y associe une valeur) directement dans le module, ce nom est ajouté au niveau de l'espace de noms des globales du module. Par exemple si l'on a écrit:

DELTA=0.03
class Acteur :
    ...
def fact(n):
    ...

On définira dans l'espace de noms des globales du module les noms: DELTA, Acteur, fact.

Lorsque l'on définit un nom (que l'on y associe une valeur) dans une fonction ou une méthode, il est par défaut créé au niveau de l'espace de noms local (espace de noms qui contient les paramètres donnés lors de l'appel à la méthode/fonction), et ce même s'il y a une globale du même nom. Pour qu'il soit défini au niveau de l'espace global, il faut le spécifier à l'interpréteur via la déclaration global. Par exemple si l'on a écrit:

def f (a) :
    global dernier
    b = a**2
    dernier = a
    return b

On stocke bien la dernière valeur de a donnée à f dans la globale dernier du module, et on a une variable b locale. Si l'on avait omis la déclaration globale, Python aurait créé une variable dernier locale et la variable du module n'aurait jamais été modifiée lors d'un appel à f.

Résolution de nom

La résolution des noms lors de l'exécution se fait dans l'ordre: locales, globales, builtins. C'est à dire que l'interpréteur recherche le nom:

  1. dans l'espace de noms des locales liées à l'appel de la méthode/fonction,
  2. dans l'espace de noms des globales du module,
  3. dans l'espace de noms des builtins.

Dans les versions de Python précédent la 2.2, la résolution des noms pour les déclarations imbriquées passait directement du niveau le plus local au niveau global (cour-circuitant les niveaux intermédiaires). Par exemple dans:

def f(a) :
    x = 3
    def g(b) :
        print x

La variable x était recherchée dans les locales de g puis directement dans les globales du module puis dans les builtins.

Depuis la version 2.2 la résolution de noms fonctionne comme on s'y attend logiquement, et prend en compte les niveaux intermédiaires (dans l'exemple le x affiché dans g est bien celui définit dans f).

Les pièges

Note: l'utilisation de pychecker permet de détecter certaines de ces erreurs.

La globale masquée

Ou l'oubli d'utiliser la spécification global lorsque l'on veut modifier une variable globale à partir d'une méthode/fonction. Un tel oubli fait que l'on travaille sur une variable créée localement par l'interpréteur alors que l'on pensait travailler sur la globale.

Le globale importée

Lorsque l'on importe un nom d'un autre module (via from module import nom ou bien via from module import *), on crée dans son propre espace de noms de son module, une variable portant le même nom. Mais contrairement aux langages comme le C, ADA, Java ou Pascal, on a créé une seconde variable, même si à sa création elle référence le même objet.

S'il s'agit d'un nom que l'on n'est pas censé ré-affecter (nom de constante, nom de classe, nom de fonction...), cela a normalement peu d'impact et permet d'accéder rapidement à l'objet souhaité. Mais s'il s'agit d'un nom pour lequel on veut éventuellement modifier l'objet référencé, cela peut amener à des situations où la globale définie dans un module que l'on a importé ne correspond pas à la même valeur que cette même globale dans notre module.

a terminer... (distinction objets mutables, ajout d'exemple, conseils pour éviter cela)

Multithreading

Le multithreading est un mécanisme permettant d'avoir, dans le même processus sur l'ordinateur, plusieurs fils d'exécution simultanés, par exemple un fil d'exécution qui gère l'interface graphique, deux fils d'exécution qui effectuent des téléchargements réseau, et un quatrième fil d'exécution qui surveille un fichier de log.

Les Threads

Vous remarquerez dans les modules Python la présence de modules thread et threading (le premier est un module de bas niveau, on utilisera plutôt le second). Dans le module threading, vous avez entre autres une classe Thread qui vous permet de créer un nouveau fil d'exécution (par défaut il existe un fil d'exécution principal ou "main thread" qui est celui avec lequel l'interpréteur Python démarre).

Note: un thread ne démarre son exécution que lorsque l'on appelle sa méthode start.

Thread sur une fonction

La création d'un nouveau thread nécessite un "point d'entrée", c'est à dire une fonction (ou méthode) sur lequel le nouveau fil d'exécution va démarrer.

#!/usr/bin/python
import threading,time

def fct(a,b) :
    print "fct(%d,%d)"%(a,b)
    time.sleep(3)
    print "fct(%d,%d)=>%d"%(a,b,a+b)
    return a+b

t = threading.Thread(target=fct,args=(3,5))
t.start()

Sous-classe de Thread

Une autre possibilité est de créer des objets à partir de sous-classes de Thread pour lesquelles on redéfinit la méthode run.

#!/usr/bin/python
import threading,time

class T (threading.Thread) :
    def __init__(self,_a,_b) :
        threading.Thread.__init__(self)
        self.a = _a
        self.b = _b
    def run(self) :
        print "T(%d,%d)"%(a,b)
        time.sleep(3)
        print "T(%d,%d)=>%d"%(a,b,a+b)
        return a+b

t = T(3,5)
t.start()       

Problème: les accès concurrents

Lorsque vous créez des threads, un gros danger est que plusieurs threads manipulent de façon non coordonnée des informations. L'exemple simple et typique est l'incrément d'un compteur a = a + 1.

Petit test avec le script suivant:

#!/usr/bin/python
# -*- coding: latin1 -*-
"""Petite démo du problème des accès concurrents.

On va incrémenter N fois une variable et la décrémenter N fois.
Si tout va bien, elle devrait revenir à sa valeur initiale.
"""

from threading import Thread
import sys
sys.setcheckinterval(1)

variable = 0
N = 100000

def fctadd() :
    global variable
    for i in xrange(N) :
        variable = variable + 1

def fctsub() :
    global variable
    for i in xrange(N) :
        variable = variable - 1

print "Valeur initiale:",variable
print "Acces sequentiel..."
fctadd()
fctsub()
print "Valeur atteinte:",variable
print "Acces concurrent..."
t1=Thread(target=fctadd)
t2=Thread(target=fctsub)
t1.start()
t2.start()
t1.join()
t2.join()
print "Valeur atteinte:",variable
print "fini."

Note: le sys.setcheckinterval(1) assure que l'interpréteur Python fera des échanges de tâche souvent.

Résultat:

[laurent@cerise dev]$ python concurrence.py
Valeur initiale: 0
Acces sequentiel...
Valeur atteinte: 0
Acces concurrent...
Valeur atteinte: -16977
fini.
[laurent@cerise dev]$ python concurrence.py
Valeur initiale: 0
Acces sequentiel...
Valeur atteinte: 0
Acces concurrent...
Valeur atteinte: -97111
fini.
[laurent@cerise dev]$ python concurrence.py
Valeur initiale: 0
Acces sequentiel...
Valeur atteinte: 0
Acces concurrent...
Valeur atteinte: -61014
fini.
[laurent@cerise dev]$ python concurrence.py
Valeur initiale: 0
Acces sequentiel...
Valeur atteinte: 0
Acces concurrent...
Valeur atteinte: 0
fini.
[laurent@cerise dev]$ python concurrence.py
Valeur initiale: 0
Acces sequentiel...
Valeur atteinte: 0
Acces concurrent...
Valeur atteinte: 97461
fini.

Comme vous le voyez, le résultat peut être bon, mais c'est par hasard. Si vous programmez avec du multithread, vous devez protéger les informations que vous modifiez contre des accès concurrents entre différents threads à l'aide des outils de synchronisation.

Outils de synchronisation

Afin de résoudre les problèmes de synchronisation entre threads, il existe différents outils adaptés aux différents besoins identifiés.

Exclusion mutuelle - Lock et RLock

Les outils d'exclusion mutuelle, communément appelés mutex, correspondent à des verrous qui permettent de protéger des ressources. Avant d'accéder à une ressource protégée un thread doit commencer par prendre le verrou, et lorsqu'il a terminé de manipuler cette ressource, il doit libérer le verrou correspondant afin qu'un autre thread puisse y accéder.

En Python le module threading propose deux verrous, Lock et RLock, nous reviendrons sur la différence entre les deux plus loin. On prend un verrou Lock par un appel à sa méthode acquire, et on le libère par un appel à sa méthode release. Comme il est essentiel de libérer les verrous que l'on a pris, on utilise généralement la séquence suivante qui assure la libération du verrou, que la sortie du code se fasse de façon normale ou via une exception:

# Soit un objet lock = threading.Lock()
try :
    lock.acquire()
    # Ici le code de traitement sur la ressource protégée par le Lock.
finally :
    lock.release()

La différence entre Lock et RLock se situe dans la capacité d'un thread à reprendre un verrou qu'il a déjà acquis. Si le cas se pose pour un verrou de type Lock, le thread sera bloqué (et le verrou définitivement verrouillé), alors que pour un verrou de type RLock, le thread peut prendre plusieurs fois le même verrou (et doit le libérer autant de fois qu'il l'a pris).

Pourquoi avoir les deux... parce qu'un verrou de type Lock est plus efficace, et dans un certain nombre de cas suffit. Mais si dans le traitement de votre ressource vous pouvez être amené à reprendre le verrou, utilisez un RLock.

Note: la méthode acquire peut prendre un paramètre que l'on met à faux pour indiquer que l'on ne veut pas se bloquer, la méthode retourne alors immédiatement un booléen indiquant si le verrou a été pris par le thread appelant (vrai), ou s'il a déjà été pris par un autre thread et n'est donc pas disponible (faux).

Au coeur de Python

Global Lock

Dans son fonctionnement interne, l'interpréteur Python supporte aussi le multithreading (en fait il s'appui sur le multithreading du système hôte pour implémenter les threads Python). Il se trouve donc confronté au même problème d'accès concurrents et au besoin de synchronisation.

Pour éviter les incohérences, l'interpréteur Python utilise un verrou global au processus, le fameux global interpreter lock (souvent abrégé en GIL). Chaque thread n'exécute des instructions de l'interpréteur manipulant les objets Python que lorsqu'il a pû prendre le verrou global pour son usage, assurant ainsi qu'aucun autre thread ne manipule ces objets.

Pour éviter de libérer et reprendre trop souvent le verrou, chaque thread essaie d'exécuter un certain nombre d'instructions lorsqu'il y est autorisé. Les fonctions getcheckinterval et setcheckinterval du module sys permettent de connaître et de modifier le nombre d'instructions traitées lorsqu'un thread possède le verrou global.

De plus, lorsqu'une opération génère un temps d'attente sans accès aux données internes de l'interpréteur (par exemple un accès disque), l'opération concernée dans la bibliothèque Python se charge de libérer le verrou global durant l'opération et de le reprendre avant de retourner à l'interpréteur.

Modules d'Extension en C

Lorsque vous écrivez un moodule d'extension en C ou C++, si vous avez des opérations longues qui ne nécessitent pas d'accéder aux objets Python, il est de bon goût de libérer vous aussi le verrou global afin de laisser les autres threads fonctionner dans l'interpréteur pendant que vous réalisez vos opérations.

Pour cela les modules d'extension disposent de deux macros: Py_BEGIN_ALLOW_THREADS et Py_END_ALLOW_THREADS, qui doivent encadrer votre section de code qui réalise les opérations longues ne nécessitant pas d'accéder aux objets Python.

Voir dans la documentation Extending and Embedding the Python Interpreter, la section 1.10.3 Thin Ice.

comment éviter le spam

pour éviter de se faire polluer par tous les chercheurs-de-references qui viennent polluer les wikis à coup de milliers d'urls, il suffit de renseigner le champ antispam situé en bas de la page d'édition.

c'est simple et de bon gout


2016-06-05 21:43