Fonctionnalités avancées de Python Version 1.0.0 David Froger 03 April 2014 Table des matières 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2 2 3 3 4 4 decorator 2.1 Comparaison avec une composition de fonction 2.2 Syntaxe des décorateurs . . . . . . . . . . . . . 2.3 Passage d’arguments . . . . . . . . . . . . . . 2.4 Exemple réel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 6 7 7 property 3.1 Attributs, mutateurs et accesseurs . 3.2 property . . . . . . . . . . . . 3.3 descriptor . . . . . . . . . . 3.4 descriptor personnalisé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 8 10 12 4 metaclass 4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 meta classes personnalisées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3 Exemples réels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 13 14 15 5 yield 5.1 range VS xrange . . 5.2 Iterator . . . . . . . . 5.3 Generator . . . . . . 5.4 Generator expression . . . . 16 16 16 17 17 with 6.1 Exemples d’utilisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2 Implémentation d’un manageur de contexte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3 Utilisation du module contextlib . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 18 19 20 2 3 6 Introduction 1.1 Fonction pour itérer sur des séquences . . . 1.2 map, reduce . . . . . . . . . . . . . . . . . 1.3 Compréhension de listes et de dictionnaires 1.4 Tout est objet . . . . . . . . . . . . . . . . 1.5 Fermeture (closure) . . . . . . . . . . . 1.6 finally . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Remerciements 20 Cette présentation traite des fonctionnalités avancées du langage Python. Souvent méconnues, elles permettent au développeur d’écrire un code plus flexible, sûr et concis. Cette courte présentation ne rentrera pas dans tous les détails à connaître pour utiliser pratiquement ces fonctionnalités, mais signalera plutôt leur existence et ce qu’elles peuvent apporter, en donnant les concepts clefs pour aborder plus facilement les documentations, livres ou articles plus complets traitant de ces sujets. Les coroutines et yield from sont passés sous silence pour le moment, et pourront faire l’objet de deux nouvelles sections dans le futur. 1 Introduction Nous commençons cette présentation de Python avancé par quelques exemples disparates de fonctionnalités de Python généralement connues, se situant à la limite entre le Python de base et le Python avancé. 1.1 Fonction pour itérer sur des séquences La fonction enumerate permet d’itérer sur les éléments d’une séquence avec leurs indices : words = [’Rien’, ’ne’, ’sert’, ’de’, ’courir’] for iword, word in enumerate(words): print iword, word 0 1 2 3 4 Rien ne sert de courir La fonction zip permet de parcourir deux séquences en même temps : words = [’Rien’, ’ne’, ’sert’, ’de’, ’courir’] lengths = [4, 2, 4, 2, 6] for iword,(word,length) in enumerate( zip(words,lengths) ): print iword, word, length 0 1 2 3 4 Rien 4 ne 2 sert 4 de 2 courir 6 1.2 map, reduce Python intègre les fonctions map et reduce : words = [’Rien’, ’ne’, ’sert’, ’de’, ’courir’] length = map(len, words) sum = reduce(lambda a,b:a+b, length) print length, sum [4, 2, 4, 2, 6] 18 1.3 Compréhension de listes et de dictionnaires La compréhension de liste permet de créer une liste à partir d’une seule expression : print [ i**2+1 for i in range(5) ] [1, 2, 5, 10, 17] Il est possible d’inclure une condition : print [ i**2+1 for i in range(5) if i%2 == 0] [1, 5, 17] On peut créer des dictionnaires de la même façon : Introduit dans la version 2.7. d = {i: i**2 + 1 for i in range(5) if i%2 == 0} for key,value in d.iteritems(): print "%i => %2i" % (key, value) 0 => 1 2 => 5 4 => 17 1.4 Tout est objet Les fonctions en Python sont des objets. On peut par exemple stocker une référence sur une fonction, et passer cette référence en argument à une autre fonction. def carre(x): return x**2 def compute(func,seq): return [ func(x) for x in seq ] func = carre seq = range(5) print compute(func,seq) [0, 1, 4, 9, 16] De même, les classes sont des objets : class Foo: def __init__(self,a,b): self.a = a self.b = b def operate(self): return self.a + self.b def compute(cls, a, b): instance = cls(a,b) return instance.operate() cls = Foo print compute(cls,10,20) 30 1.5 Fermeture (closure) Les règles de visibilité des variables permettent de créer des fermetures. Ici, la fonction add_const utilise la valeur de const définie dans l’espace de nom de la fonction qui l’englobe. def create_add_const(const): def add_const(number): return number + const return add_const add_const = create_add_const(10) print add_const(2) 12 1.6 finally Le mot clef finally permet d’exécuter du code quelque soit le résultat d’un code dans un block try : — si aucune exception n’a été levée, — si une exception a été levée et capturée, — si une exception a été levée sans être capturée. Cela permet de fermer dans le block finally une ressource ouverte (fichier, socket, ...). Voici un exemple d’utilisation de finally 1 : def divide(x, y): try: result = x / y except ZeroDivisionError: print "division by zero!" else: print "result is", result finally: print "executing finally clause" 1. Exemple tiré de la documentation officielle (https ://docs.python.org/2/tutorial/errors.html#defining-clean-up-actions) divide(2, 1) print divide(2, 0) print try: divide("2", "1") except TypeError: print "TypeError catched" result is 2 executing finally clause division by zero! executing finally clause executing finally clause TypeError catched 2 decorator Les décorateurs permettent d’annoter un code source Python pour modifier le comportement d’une fonction ou d’une classe. 2.1 Comparaison avec une composition de fonction Contrairement à la composition de fonction , dont un exemple simple est, par exemple : 𝑦 = 𝑔(𝑓 (𝑥)) et dont l’équivalent en Python serait, par exemple, def f(x): return x+20 def g(x): return x+300 print g( f(1) ) 321 l’action d’un décorateur se symbolise plutôt par 𝑦 = (𝑑(𝑓 ))(𝑥) Par exemple, la fonction d est un décorateur qui prend en argument un fonction f, et retourne une fonction nouvelle_f, qui vérifie que le nombre en argument est positif, puis appelle f : def d(f): def nouvelle_f(x): if x < 0: raise ValueError, "On n’accepte que les nombres positifs." return f(x) return nouvelle_f def f(x): return x+100 print d(f) (-1) Traceback (most recent call last): ... ValueError: On n’accepte que les nombres positifs. 2.2 Syntaxe des décorateurs L’exemple précédent peut être réécrit : def d(f): def nouvelle_f(x): if x < 0: raise ValueError, "On n’accepte que les nombres positifs." return f(x) return nouvelle_f def f(x): return x+100 f = d(f) print f(-1) Traceback (most recent call last): ... ValueError: On n’accepte que les nombres positifs. Python fournit une syntaxe pour décorer une fonction de cette manière, en annotant la ligne précédant la définition de la fonction du nom du décorateur, précédé du symbole @ : def d(f): def nouvelle_f(x): if x < 0: raise ValueError, "On n’accepte que les nombres positifs." return f(x) return nouvelle_f @d def f(x): return x+100 print f(-1) Traceback (most recent call last): ... ValueError: On n’accepte que les nombres positifs. 2.3 Passage d’arguments Il est possible d’utiliser les arguments (et arguments par mot clef) de la fonction décorée. Il est également possible de définir des décorateurs avec des arguments. 2.4 Exemple réel Voici un exemple d’application Web basé sur Flask, qui associe la fonction login à l’URL /login, pour des méthodes GET et POST 2 : @app.route(’/login’, methods=[’GET’, ’POST’]) def login(): if request.method == ’POST’: do_the_login() else: show_the_login_form() D’autre exemples réels seront rencontrés dans la suite de la présentation. 3 property Les properties permettent d’implémenter les fonctionnalités des mutateurs (setters) et accesseurs (getters) tout en ayant l’impression de manipuler directement un attribut. 3.1 Attributs, mutateurs et accesseurs Supposons que l’on crée une classe Rectangle de la sorte : class Rectangle(object): def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height r = Rectangle(10,2) print r.width, r.height, r.area() r.width *= 10 print r.width, r.height, r.area() 10 2 20 100 2 200 Bien que cette classe fonctionne, elle ne fait pas de vérification sur ses données d’entrée, et permet par exemple de créer des longueurs négatives : r = Rectangle(10,5) r.width *= -1 print r.width, r.height, r.area() 2. Exemple tiré de la documentation de Flask. (http ://flask.pocoo.org/docs/quickstart/#http-methods) -10 5 -50 Pour introduire un contrôle sur les données d’entrée, on peut choisir d’utiliser des mutateurs et des accesseurs : class Rectangle(object): def __init__(self, width, height): self._width = width self._height = height def area(self): return self._width * self._height def set_width(self,width): if width < 0: raise ValueError, "width must be positive" self._width = width def set_height(self,height): if height < 0: raise ValueError, "height must be positive" self._height = height def get_width(self): return self._width def get_height(self): return self._height r = Rectangle(10,2) print r.get_width(), r.get_height(), r.area() r.set_width( r.get_width() * 10 ) print r.get_width(), r.get_height(), r.area() try: r.set_width( r.get_width() * -1) except ValueError: print "ValueError catched" 10 2 20 100 2 200 ValueError catched Cela fonctionne mais pose un premier problème : tout code “client” utilisant la classe Rectangle est maintenant “cassé” et doit être mis à jour. Par exemple, chaque occurrence de r.width doit être remplacée par r.get_width(). Dans le cas du développement d’une bibliothèque, cela déplaira aux utilisateurs. On peut vouloir se fixer comme règle de systématiquement utiliser des mutateurs/accesseurs pour chaque attribut et se garder ainsi la possibilité de modifier les lectures et écritures des attributs. Mais cela nuit considérablement à la lisibilité du code, par exemple : r.width *= 2 devient r.set_width( r.get_width() * 2). Les properties permettent de remédier à ce problème. 3.2 property La classe Rectangle peut se réécrire ainsi : class Rectangle(object): def __init__(self, width, height): self._width = width self._height = height def area(self): return self._width * self._height def set_width(self,width): if width < 0: raise ValueError, "width must be positive" self._width = width def set_height(self,height): if height < 0: raise ValueError, "height must be positive" self._height = height def get_width(self): return self._width def get_height(self): return self._height width = property(get_width,set_width) height = property(get_height,set_height) r = Rectangle(10,2) print r.width, r.height, r.area() r.width *= 10 print r.width, r.height, r.area() try: r.width *= -1 except ValueError: print "ValueError catched" 10 2 20 100 2 200 ValueError catched Lorsqu’on lit la valeur de l’attribut width avec r.width, la méthode get_witdh est appelée, et lorsque l’on écrit dans l’attribut r.width, la méthode set_width est appelée. Cela offre à notre classe une interface commode. On peut aussi utiliser le décorateur @property : class Rectangle(object): def __init__(self, width, height): self._width = width self._height = height def area(self): return self._width * self._height @property def width(self): return self._width @width.setter def width(self,width): if width < 0: raise ValueError, "width must be positive" self._width = width @property def height(self): return self._height @height.setter def height(self,height): if height < 0: raise ValueError, "height must be positive" self._height = height r = Rectangle(10,2) print r.width, r.height, r.area() r.width *= 10 print r.width, r.height, r.area() try: r.width *= -1 except ValueError: print "ValueError catched" 10 2 20 100 2 200 ValueError catched 3.3 descriptor Plusieurs éléments de Python, comme les fonctions, les méthodes de classe, et les property, sont implémentés à l’aide de descriptor. Un descripteur est une classe qui implémente les méthodes spéciales __get__, et possiblement __set__ (et aussi possiblement __del__). Lorsque l’instance d d’un descripteur est l’attribut d’une classe cls d’instance c, accéder à c.d appelle la méthode __get__ du descripteur. On peut par exemple réécrire le décorateur intégré (built-in) de Python @property de cette façon 3 : class my_property(object): "Emulate PyProperty_Type() in Objects/descrobject.c" def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel if doc is None and fget is not None: doc = fget.__doc__ self.__doc__ = doc def __get__(self, obj, objtype=None): if obj is None: 3. Exemple tiré d’un article de la documentation officielle de Raymond Hettinger (https ://docs.python.org/2/howto/descriptor.html#properties). return self if self.fget is None: raise AttributeError("unreadable attribute") return self.fget(obj) def __set__(self, obj, value): if self.fset is None: raise AttributeError("can’t set attribute") self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: raise AttributeError("can’t delete attribute") self.fdel(obj) def getter(self, fget): return type(self)(fget, self.fset, self.fdel, self.__doc__) def setter(self, fset): return type(self)(self.fget, fset, self.fdel, self.__doc__) def deleter(self, fdel): return type(self)(self.fget, self.fset, fdel, self.__doc__) class Rectangle(object): def __init__(self, width, height): self._width = width self._height = height def area(self): return self._width * self._height @my_property def width(self): return self._width @width.setter def width(self,width): if width < 0: raise ValueError, "width must be positive" self._width = width @my_property def height(self): return self._height @height.setter def height(self,height): if height < 0: raise ValueError, "height must be positive" self._height = height r = Rectangle(10,2) print r.width, r.height, r.area() r.width *= 10 print r.width, r.height, r.area() try: r.width *= -1 except ValueError: print "ValueError catched" 10 2 20 100 2 200 ValueError catched 3.4 descriptor personnalisé Cela ouvre de nombreuses portes : plutôt que d’utiliser @property, on peut créer son propre descriptor. Par exemple, dans le cas de la classe Rectangle, un descripteur réutilisable qui n’accepte que des valeurs positives 4 : from weakref import WeakKeyDictionary class NonNegative(object): """A descriptor that forbids negative values""" def __init__(self, default): self.default = default self.data = WeakKeyDictionary() def __get__(self, instance, owner): # we get here when someone calls x.d, and d is a NonNegative instance # instance = x # owner = type(x) return self.data.get(instance, self.default) def __set__(self, instance, value): # we get here when someone calls x.d = val, and d is a NonNegative instance # instance = x # value = val if value < 0: raise ValueError("Negative value not allowed: %s" % value) self.data[instance] = value class Rectangle(object): width = NonNegative(0) height = NonNegative(0) def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height r = Rectangle(10,2) print r.width, r.height, r.area() r.width *= 10 print r.width, r.height, r.area() try: r.width *= -1 4. Exemple tiré de l’article Python Descriptors Demystified wer.ipython.org/urls/gist.github.com/ChrisBeaumont/5758381/raw/descriptor_writeup.ipynb). de Chris Beaumont (http ://nbvie- except ValueError: print "ValueError catched" 10 2 20 100 2 200 ValueError catched 4 metaclass Les méta-classes permettent de modifier la création d’un objet class (on parle bien de la création de la classe, et non de son instantiation). 4.1 Introduction Lorsque Python fait l’analyse grammaticale (parsing) de : from math import sqrt class GoldRectangle(object): gold_number = 0.5 * (1 + sqrt(5)) def __init__(self,width): self.width = width self.height = width / GoldRectangle.gold_number def area(self): return self.width * self.height print GoldRectangle r = GoldRectangle(10) print r.width, r.height, r.area() <class ’GoldRectangle’> 10 6.1803398875 61.803398875 pour créer l’objet GoldRectangle, il collecte trois informations : — le nom de la classe GoldRectangle, — les classes de base, ici on n’a que object, — les attributs de la classe, gold_number, __init__ et area, auxquels sont rajoutés certains attibuts spéciaux. Ces trois informations sont ensuite passées à une metaclass, qui est chargée de créer l’objet class (l’objet GoldRectangle que l’on a affiché). Cette metaclass est (pour les classes new-style) par défaut l’objet type qui, outre son rôle de donner le type d’un objet, est aussi utilisé pour créer des classes. Ainsi, la classe GoldRectangle créée ci-dessus peut être créée alternativement de cette façon : from math import sqrt gold_number = 0.5 * (1 + sqrt(5)) def __init__(self,width): self.width = width self.height = width / GoldRectangle.gold_number def area(self): return self.width * self.height name = "GoldRectangle" bases = (object,) attributes = {’gold_number’: gold_number, ’__init__’: __init__, ’area’:area} GoldRectangle = type(name, bases, attributes) print GoldRectangle r = GoldRectangle(10) print r.width, r.height, r.area() <class ’GoldRectangle’> 10 6.1803398875 61.803398875 L’utilisation des meta-classes consiste à remplacer la metaclass type par sa metaclass personnalisée. 4.2 meta classes personnalisées Par exemple, la méta-classe AccessorType ajoute à la classe qu’elle crée les property correspondants aux méthodes de classe commençant par les préfixes get_, set_ et del_ 5 : class AccessorType(type): def __init__(self, name, bases, d): type.__init__(self, name, bases, d) accessors = {} prefixs = ["get_", "set_", "del_"] for k in d.keys(): v = getattr(self, k) for i in range(3): if k.startswith(prefixs[i]): accessors.setdefault(k[4:], [None, None, None])[i] = v for name, (getter, setter, deler) in accessors.items(): # create default behaviours for the property - if we leave # the getter as None we won’t be able to getattr, etc.. # [...] some code that implements the above comment setattr(self, name, property(getter, setter, deler, "")) class Rectangle(object): __metaclass__ = AccessorType def __init__(self,width,height): self._width = width self._height = height def get_width(self): return self._width def set_width(self,width): 5. Exemple tiré de l’article de Eli Bendersky. (http ://eli.thegreenplace.net/2011/08/14/python-metaclasses-by-example/) self._width = width def get_height(self): return self._height def set_height(self,height): self._height = height r = Rectangle(10,2) print r.width 10 4.3 Exemples réels On rencontre souvent les méta-classes dans des bibliothèques pour créer des modèles de données. Par exemple, PyTables permet de décrire le schéma d’une table de base de donnée de cette façon 6 : class Particle(IsDescription): name = StringCol(16) idnumber = Int64Col() ADCcount = UInt16Col() TDCcount = UInt8Col() grid_i = Int32Col() grid_j = Int32Col() pressure = Float32Col() energy = Float64Col() # # # # # # # # 16-character String Signed 64-bit integer Unsigned short integer unsigned byte 32-bit integer 32-bit integer float (single-precision) double (double-precision) SQLAlchemy permet de faire 7 : from sqlalchemy import Column, Integer, String class User(Base): __tablename__ = ’users’ id = Column(Integer, primary_key=True) name = Column(String) fullname = Column(String) password = Column(String) def __repr__(self): return "<User(name=’%s’, fullname=’%s’, password=’%s’)>" % ( self.name, self.fullname, self.password) WTForms permet de créer des formulaires HTML avec 8 : from wtforms import Form, BooleanField, StringField, validators class RegistrationForm(Form): username = StringField(’Username’, [validators.Length(min=4, max=25)]) email = StringField(’Email Address’, [validators.Length(min=6, max=35)]) accept_rules = BooleanField(’I accept the site rules’, [validators.InputRequired()]) 6. Exemple tiré du tutoriel officiel de PyTables. (http ://pytables.github.io/usersguide/tutorials.html#declaring-a-column-descriptor) 7. Exemple tiré du tutoriel officiel de SQLalchemy. (http ://docs.sqlalchemy.org/en/rel_0_9/orm/tutorial.html#create-a-schema) 8. Exemple tiré du tutoriel officiel de WTForms. (http ://wtforms.readthedocs.org/en/latest/crash_course.html#getting-started) 5 yield Le mot clef yield permet de créer facilement des generator, qui sont des fonctions itérables. 5.1 range VS xrange La fonction range crée une liste de N éléments, sur laquelle il est possible d’itérer : for i in range(4): print i 0 1 2 3 La fonction xrange, en revanche, n’alloue pas de l’espace mémoire pour les N éléments, mais pour un seul élément. Il est possible d’itérer de la même manière : for i in xrange(4): print i 0 1 2 3 5.2 Iterator Un iterator est une classe qui implémente les méthodes spéciales __iter__ et next, pour devenir itérable. La fonction xrange peut être implémentée de cette façon : class my_xrange: def __init__(self, last): self.current = 0 self.last = last def __iter__(self): return self def next(self): if self.current >= self.last: raise StopIteration self.current += 1 return self.current - 1 for i in my_xrange(4): print i 0 1 2 3 La classe my_xrange implémente __iter__ qui doit retourner l’iterateur en question. Dans notre cas simple, il s’agit de l’instance elle-même, mais pour un conteneur, il retournerait une valeur différente de self). La fonction next retourne la valeur de l’itération suivante, et lance l’exception StopIteration pour signaler la fin de la boucle. 5.3 Generator my_xrange peut être implémentée plus aisément avec un générateur, qui est une simple fonction utilisant le mot clef yield. Lorsque l’instruction yield est atteinte, l’execution du programme est transférée du générateur à l’appelant, et est redonnée au générateur lors de la transition à l’itération suivante de la boucle de l’appelant. def my_xrange(last): current = 0 while current < last: yield current current += 1 for i in my_xrange(4): print i 0 1 2 3 5.4 Generator expression À l’instar des compréhensions de liste : for x in [ i**2 for i in range(7) if i%2 == 0]: print x 0 4 16 36 il est possible d’utiliser les generator expression pour ne stocker en mémoire qu’un seul élément : for x in ( i**2 for i in range(7) if i%2 == 0): print x 0 4 16 36 6 with Le mot clef with est utilisé par les manageurs de contexte, qui permettent d’ouvrir et de libérer une ressource (de la même manière que finally) de façon fiable. 6.1 Exemples d’utilisation Par exemple, le mot clef with peut s’utiliser pour ouvrir une ressource (ici un fichier), et la fermer automatiquement en sortant du contexte d’exécution de with, qu’une exception ait été levée ou non. def foo(f): pass def bar(f): raise ValueError with open(’file.txt’,’w’) as f: foo(f) with open(’file.txt’,’w’) as f: bar(f) Dans les deux cas, le fichier file.txt est fermé. Un verrou utilisé par des threads peut être acquis et libéré avec les méthodes aquire() et release(), mais également avec un manageur de contexte. Ainsi from threading import Thread, Lock import time class Worker(Thread): id_ = 0 def __init__(self,lock): Thread.__init__(self) self.lock = lock self.id_ = Worker.id_ Worker.id_ += 1 def run(self): self.lock.acquire() print ’Worker %i has acquired lock’ % self.id_ time.sleep(0.1) self.lock.release() print ’Worker %i has released lock’ % self.id_ lock = Lock() w0 = Worker(lock) w1 = Worker(lock) w0.start() time.sleep(0.01) w1.start() w0.join() w1.join() Worker Worker Worker Worker 0 0 1 1 has has has has peut se réécrire acquired released acquired released lock lock lock lock from threading import Thread, Lock import time class Worker(Thread): id_ = 0 def __init__(self,lock): Thread.__init__(self) self.lock = lock self.id_ = Worker.id_ Worker.id_ += 1 def run(self): with self.lock: print ’Worker %i has acquired lock’ % self.id_ time.sleep(0.1) print ’Worker %i has releases lock’ % self.id_ lock = Lock() w0 = Worker(lock) w1 = Worker(lock) w0.start() time.sleep(0.01) w1.start() w0.join() w1.join() Worker Worker Worker Worker 0 0 1 1 has has has has acquired releases acquired released lock lock lock lock 6.2 Implémentation d’un manageur de contexte Pour être utilisable avec with, une ressource doit implémenter les méthodes spéciales __enter__, qui ouvre la ressource, et __exit__, qui ferme la ressource (et possiblement gère une exception levée) : class Ressource: def __init__(self): self._opened = False def open(self): print "open ressource" self._opened = True def close(self): print "close ressource" self._opened = False def use(self): print "use ressource" def __enter__(self): self.open() return self def __exit__(self,exc_type,exc_value,traceback): self.close() with Ressource() as r: r.use() open ressource use ressource close ressource 6.3 Utilisation du module contextlib Le module contextlib permet également d’implémenter un manageur de contexte en décorant un générateur à l’aide de @contextmanager. Par exemple : from contextlib import contextmanager class Ressource: def __init__(self): self._opened = False def open(self): print "open ressource" self._opened = True def close(self): print "close ressource" self._opened = False def use(self): print "use ressource" @contextmanager def ressource(): r = Ressource() r.open() yield r r.close() with ressource() as r: r.use() open ressource use ressource close ressource 7 Remerciements Cédric Doucet pour la relecture.