| QuestionsGenerales |
UserPreferences |
| Wiki Python Fr | FrontPage | RecentChanges | TitleIndex | WordIndex | SiteNavigation | HelpContents | moin.sf.net |
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.
>>> s = "Bonjour" >>> id(s) 134875344 >>> s += " a vous." >>> id(s) 134859448Ce 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.
>>> l = [ 1,2,3,4 ] >>> id(l) 134865140 >>> l += [ 5,6,7,8 ] >>> id(l) 134865140Les 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...).
>>> 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)
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 quel façon Python parcours 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.
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éfinit 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 global, Python aurait créé une variable dernier locale et la variable du module n'aurait jamais été modifiée lors d'un appel à f. 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 exemples 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).
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)
Note: un thread ne démarre son exécution que lorsque l'on appelle sa méthode start.
1 2 3 4 5 6 7 8 9 10 11 | #!/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() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #!/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() |
Petit test avec le script suivant:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | #!/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 tache 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.
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).
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.
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.