Orientation objet: les classes et leurs instances

  • Tout est un objet (entiers, fonctions, modules, sockets ...)
  • Type structurés : classes et instances de classes
  • On accède aux attributs et aux méthodes d'une classe avec dir()
  • En python 2.x : "old" and "new" styles
  • Les classes newstyle héritent du type object
  • Syntaxe : class Toto: ou class Toto(object):
In [1]:
class Toto: pass # old style

a = Toto()        # Création d'une instance
dir(a)
Out[1]:
['__doc__', '__module__']
In [2]:
# pas grand chose à voir. Avec le "newstyle" (ou en python 3) :
class Titi(object): pass

b = Titi()
print dir(b)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

On voit qu'il y a déjà pas mal d'attributs prédéfinis.
Les attributs commençant et finissant par deux soulignés sont les méthodes spéciales (ou attributs spéciaux). Ils ne sont pas destinés à être appelés tels quels (on écrira len(x) plutôt que $\tt x.\_\_len\_\_()$

Ils permettent la surcharge des opérateurs.

In [3]:
class Toto:
    def __len__(self): return 42
a = Toto()

print  len(a)
42

Les méthodes sont définies comme des fonctions ordinaires. Leur premier argument doit toujour être l'instance appelante, conventionnellement appelée self (équivalent de this dans d'autres langages).

La méthode spéciale $\tt \_\_init\_\_$ sert à initialiser les instances. Elle est applée imédiatement après la création d'une instance. Son premier argument est l'instance elle même, les suivants sont les paramètres éventuels :

In [4]:
class Client(object):
    def __init__(self,nom,prenom):
        self.nom = nom
        self.prenom = prenom

c = Client('Garcin','Lazare')
print c.nom, c.prenom
Garcin Lazare

Héritage

À la place de object, on peut passer en argument n'importe quel type existant. La classe crée héritera de ses attributs, et on pourra les modifier si nécessaire. S'il y a des paramètres à initlaliser, on doit appeler la méthode $\tt\_\_init\_\_$ de la classe parent.

In [5]:
class Blah(list):
    def __init(self,ll):
        return list.__init__(self,ll)

    def __mul__(self,mm):
        return Blah([x*y for x in self for y in mm])

a = Blah([2,3,4]); b = Blah([5,10]); 
print a, b, a+b, a*b
[2, 3, 4] [5, 10] [2, 3, 4, 5, 10] [10, 20, 15, 30, 20, 40]

On voit que Blah a hérité de l'addition des listes. On lui a seulement rajouté une méthode de multiplication, qui définit l'opération $*$.

Les instances des classes dérivées sont vues comme des instances de leurs parents.

In [6]:
class Cbase: pass
class Cderivee(Cbase): pass
Ibase=Cbase() ; Iderivee=Cderivee()
print isinstance(Ibase,Cbase)
print isinstance(Iderivee,Cbase)

print isinstance(Ibase,Cderivee)
True
True
False

In [7]:
# Autre exemple
class defaultdict(dict):
    """ Renvoie une valeur par défaut 
    si une clef n'est pas affectée """

    def __init__(self, default=None):
        dict.__init__(self)
        self.default = default

    def __getitem__(self, key):  # Noter l'usage de try...except plutôt qu'un test 
        try:
            return dict.__getitem__(self, key)
        except KeyError:
            return self.default

d = defaultdict(0.0)
e={1:4.5, 2:7.8}
d.update(e)
print d
print d[5]
{1: 4.5, 2: 7.8}
0.0

Les classes "newstyle" possèdent une méthode $\tt\_\_new\_\_$ qui prend en charge la construction de l'instance. Elle est utile pour sous-classer les types non mutables :

In [8]:
class CapString(str):
    def  __new__(cls,s):
        return  str.__new__(cls,s.lower().capitalize())

    def __add__(self,x):
        return CapString (str.__add__(self,x.lower()))
    
x=CapString("toTO")
y = CapString("tITi")
z = x+y
print x,y,z
print type(x), type(y), type(z)
Toto Titi Tototiti
<class '__main__.CapString'> <class '__main__.CapString'> <class '__main__.CapString'>

L'attribut $\tt\_\_class\_\_$ d'un objet fait référence à la classe dont il est une instance. Il permet aux instances d'accéder aux attributs de la classe elle même.

La classe suivante possède un compteur qui compte le nombre de ses instances.

In [9]:
class counter:
    count = 0            

    def __init__(self):
        self.__class__.count += 1 

print counter
print counter.count
__main__.counter
0

In [10]:
c = counter()
print c.count                           
print counter.count
1
1

In [11]:
d = counter()                     
print d.count
print c.count
print counter.count
2
2
2

count est un attribut de la classe counter.

$\tt\_\_class\_\_$ est un attribut prédéfini de toute instance d'une classe. C'est une référence à la classe dont self. est une instance.

count est accessible par référence directe à la classe, avant même la création d'une instance. Chaque instanciation incrémente count, ce qui affecte la classe elle-même.

Sans l'attribut $\tt\_\_class\_\_$, on aurait

In [12]:
class compteur:
    compte = 0
    def __init__(self):
        self.compte +=1

a=compteur()
print a.compte
b=compteur()
print b.compte
print compteur.compte
1
1
0

Public, privé, protégé

Python distingue les attributs publics et privés. Sont privés tous ceux qui commencent (mais ne finissent pas) par un double souligné.

In [13]:
class SomeClass(object):
    def PublicMethod(self):
        self.__private_field = "encapsulated"
        return self.__PrivateMethod()

    def __PrivateMethod(self):
        return self.__private_field

c = SomeClass()
print c.PublicMethod()                             
encapsulated

In [14]:
print c.__PrivateMethod() 
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-14-1b74c83997ad> in <module>()
----> 1 print c.__PrivateMethod()

AttributeError: 'SomeClass' object has no attribute '__PrivateMethod'
In [15]:
print c.__private_field 
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-15-eceaa84a5f6c> in <module>()
----> 1 print c.__private_field

AttributeError: 'SomeClass' object has no attribute '__private_field'
In [16]:
# En fait, on peut, mais il ne faut pas ...
c._SomeClass__PrivateMethod()
Out[16]:
'encapsulated'

Python n'a pas d'attributs protégés, c'est à dire accessibles seulement dans les classes dérivées. L'usage est de les préfixer par un simple souligné, de manière à ce que les utilisateurs d'une classe comprennent qu'il s'agit d'un détail d'implémentation, mais qu'ils restent accessibles aux classes dérivées.

In [17]:
class BaseClass(object):
    def _ProtectedMethod(self):
        self._protected = "protected"

class DerivedClass(BaseClass):
    def PublicMethod(self):
        self._ProtectedMethod()
        print self._protected
    def __PrivateMethod(self):
        return self.__private_field

d = DerivedClass()
d.PublicMethod()
protected

Propriétés

Python supporte les propriétés, c'est à dire, les couples de méthodes fget/fset qui sont appelées de manière transparente lorsqu'on accède à un attribut.

In [18]:
class SomeClass(object):
    def __init__(self, initial_value):
        self.__read_write_prop = initial_value
        self.__read_only_prop = initial_value

    def __GetReadWriteProp(self):
        print "Someone's reading ReadWriteProp"
        return self.__read_write_prop

    def __SetReadWriteProp(self, new_value):
        print "Someone's writing ReadWriteProp"
        self.__read_write_prop = new_value

    ReadWriteProp = property(fget=__GetReadWriteProp,
                             fset=__SetReadWriteProp)

    def __GetReadOnlyProp(self):
        print "Someone's reading ReadOnlyProp"
        return self.__read_only_prop

    ReadOnlyProp = property(fget=__GetReadOnlyProp)
In [19]:
c = SomeClass("initial")
val = c.ReadWriteProp
print "val = ",val
Someone's reading ReadWriteProp
val =  initial

In [20]:
c.ReadWriteProp = "new"
Someone's writing ReadWriteProp

In [21]:
val = c.ReadWriteProp
Someone's reading ReadWriteProp

In [22]:
print val
new

In [23]:
val = c.ReadOnlyProp
Someone's reading ReadOnlyProp

In [24]:
val
Out[24]:
'initial'
In [25]:
c.ReadOnlyProp = "new"  
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-25-31ee050b85aa> in <module>()
----> 1 c.ReadOnlyProp = "new"

AttributeError: can't set attribute

Attributs statiques et méthodes statiques

N'ont pas besoin de connaître l'instance ni la classe.

In [26]:
class InstanceCounter(object):
    num_instances = 0

    def __init__(self):
        InstanceCounter.num_instances += 1

    def GetNumInstances():    # pas de paramètre self
        return InstanceCounter.num_instances

    GetNumInstances = staticmethod(GetNumInstances) # déclare une méthode statique

a = InstanceCounter()
b = InstanceCounter()
print InstanceCounter.GetNumInstances()    # c'est 2
InstanceCounter.num_instances = 100
c = InstanceCounter()
print InstanceCounter.GetNumInstances()    # maintenant, c'est 101

print a.num_instances    
a.num_instances = 5    # modifie l'attrbut de l'instance
print a.num_instances                    
print InstanceCounter.GetNumInstances()   # mais pas l'attribut statique de la classe
2
101
101
5
101

Méthodes de classes

Elle n'ont besoin que de connaître les paramètres de leur classe, qui leur est donnée par l'argument cls>/tt> (au lieu de self), pas ceux des instances. Le mécanisme est similaire.

In [27]:
class SomeClass(object):
    def ClassMethod(cls):
        print cls.__name__  # c'est le nom de la classe ...

    ClassMethod = classmethod(ClassMethod) # déclaration d'une méthode de classe

class DerivedClass(SomeClass):
    pass

SomeClass.ClassMethod()    # répond "SomeClass"
c = SomeClass()
c.ClassMethod()            # encore "SomeClass"
DerivedClass.ClassMethod() # répond  "DerivedClass"
d = DerivedClass()
d.ClassMethod()            # encore "DerivedClass"
SomeClass
SomeClass
DerivedClass
DerivedClass

Les exceptions

Mots clés : try - except - raise - finally

In [28]:
def f():
    try:
        print "On essaie ..."
        return 1/0
    except:
        print "C'est raté ..."
        return 2+[2]
    finally:
        return 42

f()
On essaie ...
C'est raté ...

Out[28]:
42

La clause finally permet de terminer proprement (en refermant fichiers, sockets, etc. par exemple). Elle sera exécutée quoi qu'il arrive (et avant les éventuels gestionnaires d'exception).

On peut capturer les exceptions :

In [29]:
try:
    print 1/0
except Exception, e:
    print e
integer division or modulo by zero

In [30]:
try:
    s = open('toto.txt').read()
except Exception, e:
    print e
[Errno 2] No such file or directory: 'toto.txt'

Les exceptions prédéfinies sont décrites dans la documentation du module exceptions.

In [31]:
try:
    raise EnvironmentError(666, 'External program crashed', 'hello.o')
except EnvironmentError, e:
    print e
    print e.args 
    print e.errno, e.strerror, e.filename
[Errno 666] External program crashed: 'hello.o'
(666, 'External program crashed')
666 External program crashed hello.o

On peut définir de nouvelles exceptions :

In [32]:
class StupidError(Exception):
    def __init__(self):
         self.errno = 666
    def __str__(self):
        return "Stupid Error"

try:
        raise StupidError
except Exception,e:
        print e
        print e.errno
Stupid Error
666

Commentaires et docstrings

Le commentaires ordinaires commencent par des #. Une chaîne flottant à l'intérieur d'un programme est vue comme un commentraire. Juste après une définition, elle esyt vue comme une docstring.

In [33]:
# commentaires avec des dièses

"ou bien avec des chaînes littérales"

""" ou encore, sur plusieurs lignes,
avec des triple-quotes """

def racine_carree(x):
    """Cette fonction calcule la racine carrée
    du nombre fourni comme paramètre."""
    return x**0.5

help(racine_carree)
Help on function racine_carree in module __main__:

racine_carree(x)
    Cette fonction calcule la racine carrée
    du nombre fourni comme paramètre.


In [34]:
print racine_carree.__doc__
Cette fonction calcule la racine carrée
    du nombre fourni comme paramètre.

L'itération en Python

Syntaxe :

for i in iterable_object: do_something

Les listes, tuples, chaînes et dictionnaires sont itérables.

In [35]:
for x in range(10): print x*x,
print

h = {"un":1, "deux":2, "trois":3}
for key in h: print key,
print

s = 'gabuzomeu'
for c in s: print ord(c),
0 1 4 9 16 25 36 49 64 81
un trois deux
103 97 98 117 122 111 109 101 117

Les objets itérables possèdent une méthode $\tt\_\_iter\_\_()$ qui renvoie un itérateur.

On l'appelle au moyen de la fonction iter

Un itérateur possède une méthode next() qui renvoie l'élément suivant.

In [36]:
ll = [1, 2, 3, 4, 5]
it = ll.__iter__()

print it.next(), it.next()
1 2

Normalement, on écrit plûtôt

In [37]:
ll = [1, 2, 3, 4, 5]
it = iter(ll)
print it.next()
print it.next()
for x in it: print x,
1
2
3 4 5

On remarque que l'itérateur se consume au fur et à mesure que next est appelée. Si on continue :

In [38]:
it.next()
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-38-54f0920595b2> in <module>()
----> 1 it.next()

StopIteration: 
for x in obj:
   # faire quelque chose

est équivalent à

_iter = iter(obj)
while 1:
    try:
        x = _iter.next()
    except StopIteration:
        break
    # faire quelque chose

Les générateurs permettent de créer des itérateurs sur des objets qui n'ont pas besoin d'être contruits à l'avance. La syntaxe est identique à celle des fonctions ordinaires, avec le mot clé yield au lieu de return. La fonction retourne alors un générateur.

In [39]:
def fib(): # suite de Fibonacci
    a, b = 0, 1
    while 1:
        yield b; a, b = b, a+b

F=fib()

print [F.next() for i in range(10)]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

In [40]:
print [F.next() for i in range(10)] # On continue
[89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]

L'exécution est stoppée après yield et reprend à l'appel suivant de next().

Expressions generatrices

Lorsqu'on remplace les crochets par des parenthèses autour d'une liste en compréhension, on obtient un générateur :

In [41]:
g = (x*x for x in range(10))
In [42]:
for i in range(11): print g.next(),
0 1 4 9 16 25 36 49 64 81
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-42-ba2bcb1caceb> in <module>()
----> 1 for i in range(11): print g.next(),

StopIteration: 

On peut définir des classes qui supportent l'itération : il suffit d'implémenter les méthodes $\tt\_\_iter\_\_$ et next:

In [43]:
class countdown(object):

    def __init__(self,n):
        self.count = n
        
    def __iter__(self):
        return self
    
    def next(self):
        if self.count <= 0:
            raise StopIteration
        r = self.count
        self.count -= 1
        return r


c=countdown(10)
print c.next()

print list(c)  # la conversion en liste consume l'itérateur
 10
[9, 8, 7, 6, 5, 4, 3, 2, 1]

Manipulation de fichiers

La fonction open permet de créer ou de modifier des objets de type fichier (file objects). Un fichier peut être ouvert dans les modes 'r','w','a','rw' (read,write,append,read-write) et 'b' (binary, pour windows). En python 3, c'est un peu différent, il faut préciser un encodage.

Ces file objects ont des méthodes read et write.

Il ne faut pas oublier de refermer un fichier ouvert avec la méthode close().

In [44]:
f = open('bla.txt','w')
f.write('Ga\nBu\nZo\nMeu\n')
f.close()
In [45]:
!cat bla.txt
Ga
Bu
Zo
Meu

In [46]:
s = open('bla.txt','r').read() # lit tout le fichier sous forme d'une chaîne
s
Out[46]:
'Ga\nBu\nZo\nMeu\n'
In [47]:
ll = open('bla.txt').readlines() # retourne une liste de lignes
ll
Out[47]:
['Ga\n', 'Bu\n', 'Zo\n', 'Meu\n']
In [48]:
for line in open('bla.txt'): print line, # on peut itérer sur un fichier ouvert
Ga
Bu
Zo
Meu

In [49]:
with open('bla.txt','a') as f: # On utilise de préférence cette syntaxe pour éviter d'oublier de refermer le fichier
    f.write('toto')            # open() retourne un "context manager", la variable f n'existe que dans le bloc indenté
In [50]:
!cat bla.txt
Ga
Bu
Zo
Meu
toto