M1 Informatique - Python - Cours 2

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
  • En python 3 les classes héritent automatiquement dy type object
  • Syntaxe : class Toto: ou class Toto(object):
In [1]:
class Toto: pass # Attributs comme les new syle du Python 2

a = Toto()        # Création d'une instance
print(dir(a))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
In [2]:
print(dir(object))
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

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 x__len__()).

Ils permettent la surcharge des opérateurs.

In [2]:
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, mais self n'est pas un mot réservé).

La méthode spéciale __init__ sert à initialiser les instances. Elle est appelé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 [3]:
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
In [4]:
c.__dict__
Out[4]:
{'nom': 'Garcin', 'prenom': '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 à initialiser, on doit appeler la méthode __init__ de la classe parent.

In [11]:
class Vector(tuple):
    def __add__(self,y):
        return Vector([a+b for (a,b) in zip(self,y)])
In [12]:
# noter au passage la fonction zip (qui retourne un itérateur)
list(zip('abc','123'))
Out[12]:
[('a', '1'), ('b', '2'), ('c', '3')]
In [13]:
x = Vector((1,2,3))
y = Vector((4,5,6))
x+y
Out[13]:
(5, 7, 9)
In [14]:
(1,2,3)+(4,5,6)
Out[14]:
(1, 2, 3, 4, 5, 6)
In [15]:
print(x,y)
(1, 2, 3) (4, 5, 6)
In [17]:
# Tableau à m lignes et n colonnes à partir d'une liste de mn éléments
class Table(dict):
    def __init__(self,m,n,*data):
        self.rows = m
        self.cols = n
        if data:
            assert len(data) == m*n
            for i in range(m):
                for j in range(n): self[i,j] = data[n*i+j]
        else: 
            for i in range(m):
                for j in range(n): self[i,j] = 0
                    
    def __str__(self):
        return '\n'.join([str([self[i,j] for j in range(self.cols)]) for i in range(self.rows)])
            
            
In [18]:
A = Table(2,3,1,2,3,4,5,6)
print (A)
A
[1, 2, 3]
[4, 5, 6]
Out[18]:
{(0, 0): 1, (0, 1): 2, (0, 2): 3, (1, 0): 4, (1, 1): 5, (1, 2): 6}
In [20]:
# Un exemple de listes qui peuvent se multiplier
class Blah(list):
    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 [21]:
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 [22]:
# 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 de Python 3 possèdent une méthode __new__ qui prend en charge la construction de l'instance.

Elle est utile pour sous-classer les types non mutables :

In [23]:
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 __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 [24]:
class counter:
    count = 0            

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

print (counter) 
print (counter.count)
<class '__main__.counter'>
0
In [25]:
c = counter()
print (c.count)                           
print (counter.count)
1
1
In [26]:
d = counter()                     
print (d.count)
print (c.count)
print (counter.count)
2
2
2

count est un attribut de la classe counter.

__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 __class__ on aurait

In [27]:
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 [28]:
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 [29]:
print (c.__PrivateMethod())
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-29-6565485ea5a1> in <module>()
----> 1 print (c.__PrivateMethod())

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

AttributeError: 'SomeClass' object has no attribute '__private_field'
In [31]:
# En fait, on peut, mais il ne faut pas ...
c._SomeClass__PrivateMethod()
Out[31]:
'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 [32]:
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 [33]:
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 [34]:
c = SomeClass("initial")
val = c.ReadWriteProp
print ("val = ",val)
Someone's reading ReadWriteProp
val =  initial
In [35]:
c.ReadWriteProp = "new"
Someone's writing ReadWriteProp
In [36]:
val = c.ReadWriteProp
Someone's reading ReadWriteProp
In [37]:
print (val)
new
In [38]:
val = c.ReadOnlyProp
Someone's reading ReadOnlyProp
In [39]:
val
Out[39]:
'initial'
In [40]:
c.ReadOnlyProp = "new"  
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-40-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, et ne peuvent pas modifier l'état de leur classe.

In [42]:
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 (au lieu de self), pas ceux des instances. Le mécanisme est similaire. Elles peuvent modifier les paramètres de leur classe.

In [43]:
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 [44]:
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[44]:
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 [45]:
try:
    print (1/0)
except Exception as e: # différent de Python 2 ici
    print (e)
division by zero
In [46]:
try:
    s = open('toto.txt').read()
except Exception as 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 [47]:
try:
    raise EnvironmentError(666, 'External program crashed', 'hello.o')
except EnvironmentError as 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 [48]:
class StupidError(Exception):
    def __init__(self):
         self.errno = 666
    def __str__(self):
        return "Stupid Error"

try:
        raise StupidError
except Exception as 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 [49]:
# 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 [50]:
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 [51]:
for x in range(10): print (x*x),
print()

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

s = 'gabuzomeu'
for c in s: print (ord(c),end=' ')
0
1
4
9
16
25
36
49
64
81

un deux trois 
103 97 98 117 122 111 109 101 117 

Les objets itérables possèdent une méthode __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 [52]:
ll = [1, 2, 3, 4, 5]
it = ll.__iter__()

print (next(it), next(it)) # différent de Python 2
1 2

Normalement, on écrit plutôt

In [53]:
ll = [1, 2, 3, 4, 5]
it = iter(ll)
print (next(it))
print (next(it))
for x in it: print (x,end =' ')
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 [54]:
next(it)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-54-2cdb14c0d4d6> in <module>()
----> 1 next(it)

StopIteration: 
for x in obj:
   # faire quelque chose

est équivalent à

_iter = iter(obj)
while 1:
    try:
        x = next(_iter)
    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 [55]:
def fib(): # suite de Fibonacci
    a, b = 0, 1
    while 1:
        yield b; a, b = b, a+b

F=fib()

print ([next(F) for i in range(10)])
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
In [56]:
print ([next(F) 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 [57]:
g = (x*x for x in range(10))
In [58]:
for i in range(11): print (next(g), end=' ')
0 1 4 9 16 25 36 49 64 81 
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-58-f8efc0009ee4> in <module>()
----> 1 for i in range(11): print (next(g), end=' ')

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 [59]:
class countdown(object):

    def __init__(self,n):
        self.count = n
        
    def __iter__(self):
        return self
    
    def __next__(self): # différent
        if self.count <= 0:
            raise StopIteration
        r = self.count
        self.count -= 1
        return r


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

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 [60]:
f = open('bla.txt','w')
f.write('Ga\nBu\nZo\nMeu\n')
f.close()
In [61]:
!cat bla.txt
Ga
Bu
Zo
Meu
In [62]:
s = open('bla.txt','r').read() # lit tout le fichier sous forme d'une chaîne
s
Out[62]:
'Ga\nBu\nZo\nMeu\n'
In [63]:
ll = open('bla.txt').readlines() # retourne une liste de lignes
ll
Out[63]:
['Ga\n', 'Bu\n', 'Zo\n', 'Meu\n']
In [64]:
for line in open('bla.txt'): print (line, end=' ') # on peut itérer sur un fichier ouvert
Ga
 Bu
 Zo
 Meu
 
In [65]:
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 [66]:
!cat bla.txt
Ga
Bu
Zo
Meu
toto
In [67]:
!cat blu.txt
Il est plus facile de se laver les dents dans un verre à pied que de se laver les pieds dans un verre à dents.
In [68]:
open('blu.txt','r', encoding='utf8').read() 
Out[68]:
'Il est plus facile de se laver les dents dans un verre à pied que de se laver les pieds dans un verre à dents.\n'
In [69]:
open('blu.txt','rb').read() 
Out[69]:
b'Il est plus facile de se laver les dents dans un verre \xc3\xa0 pied que de se laver les pieds dans un verre \xc3\xa0 dents.\n'
In [ ]:
 
In [ ]: