Objets et références

Objets et références en Python

Frédéric Herbreteau, Bordeaux INP/LaBRI (frederic.herbreteau@bordeaux-inp.fr)

Classes et objets

Importance du typage

Représentation d’un nombre rationnel par un tuple

>>> r1 = (3, 4)  # 3/4
>>> def eval(r):
...     return r[0]/r[1]
>>> eval(r1)
0.75
>>> t1 = (1,0)
>>> eval(t1)
...
ZeroDivisionError: division by zero
>>> t2 = (1,)
>>> eval(t2)
...
IndexError: tuple index out of range

Qu’est-ce qu’un type?

Classes et objets

  • classe: structure de données + méthodes permettant de les manipuler
class Rational:
    def print(self):
        ...
  • objet: instance d’une classe
>>> r = Rational(4, 5)  # 4/5
>>> r.print()
  • abstraction:
    • la structure est uniquement manipulée via ses méthodes
    • bonne pratique: pas d’accès direct aux champs débutant par _

Construction d’un objet (méthode __init__)

  • allocation mémoire pour stocker l’instance (gérée automatiquement)
  • initialisation de l’instance courante self dans un état cohérent
class Rational:
    def __init__(self, n=0, d=1):
        assert d > 0
        self._n = n  # crée la variable d'instance `_n` et l'initialise
        self._d = d  # crée la variable d'instance `_d` et l'initialise
>>> r1 = Rational()      # 0/1
>>> r2 = Rational(2, 3)  # 2/3
>>> r2
<__main__.Rational object at 0x1017c7890>

NB: l’instance est crée par __new__ qui dépasse le cadre de ce cours

Définir une méthode

  • fonction définie dans la classe
  • dont le premier paramètre est l’instance self
class Rational:
     def __init__(self, n=0, d=1):
        assert d > 0
        self._n = n
        self._d = d

    def print(self):
        print(f'{self._n}/{self._d}')
>>> r1 = Rational(4, 6)
>>> r1.print()
4/6

Opérateurs et méthodes

  • Les opérateurs appellent des méthodes particulières: les hooks
>>> 3 + 2
5
>>> int(3).__add__(2)
5
>>> l = [1, 2, 3]
>>> l + 4
[1,2,3,4]
>>> l.__add__([4])
[1,2,3,4]
>>> 'abc' + 'de'
'abcde'
>>> 'abc'.__add__('de')
'abcde'

Définir un opérateur

  • le hook __add__ permet de définir l’opérateur +
class Rational:
    ...
    def __add__(self, r):
        return Rational(self._n  * r._d + r._n * self._d, self._d * r._d)
>>> r1 = Rational(2, 3)
>>> r2 = Rational(4, 5)
>>> r3 = r1 + r2
>>> r3.print()
22/15

Affichage des objets

  • le hook __str__ définit la représentation de l’objet comme str
  • le hook __repr__ fournit une représentation str interprétable par eval
class Rational:
    ...
    def __str__(self):
        return f'{self._n}/{self._d}'
    def __repr__(self):
        return f'Rational({self._n}, {self._d})'
>>> r1 = Rational(2,3)
>>> print(r1)   # appelle __str__
2/3
>>> r1          # appelle __repr__
Rational(2,3)

Documentation des objets

  • lister les méthodes d’un objet ou d’une classe avec dir
>>> x = 'abc'
>>> dir(x)
>>> x.upper()
'ABC'
>>> x = 3
>>> dir(x)
>>> x.bit_count()
2
  • documentation avec help
help(x)
help(x.bit_count)

Variables et références

Variables et références

  • Les variables stockent une référence vers un objet
  • les valeurs sont typées, mais les variables ne sont pas typées
>>> x = 1
>>> l = [1,2,3]
>>> s = {'a', 'b', 'c'} 

Types muables et immuables

  • les types int, bool, float, str et tuple sont immuables
  • les opérations créent une nouvelle valeur (affectation mémoire)
>>> x = 1
>>> x+1
  • list, set, dict, … et les types définis sont muables
  • certaines opérations modifient l’objet en place
>>> l1 = [1,2,3]
>>> l1.append(4)

Affectations et références

  • l’affectation d’une valeur à une variable modifie la référence
>>> x = 1
>>> x = x+1
>>> l1 = [1,2,3]
>>> l1 = [1]

Références et partage d’objets

  • plusieurs variables peuvent référencer le même objet (aliasing)
  • l’affectation crée de l’aliasing
>>> a = []
>>> b = a           # b est un alias de a
>>> a.append(1)
>>> b
[1]

Cloner un objet pour éviter l’aliasing (module copy)

  • copie superficielle: copy.copy() et copie profonde: copy.deepcopy(), ainsi que méthode copy pour certaines classes (list, dict,…)
  • redéfinir les hooks __copy__ et __deepcopy__ au besoin
>>> a = [[]]
>>> b = copy.copy(a)      # b est une copie superficielle de a (ou b = a.copy())
>>> id(b) == id(a)        # False
>>> id(b[0]) == id(a[0])  # True
>>> c = copy.deepcopy(a)  # c est une copie profonde de a
>>> id(c) == id(a)        # False
>>> id(c[0]) == id(a[0])  # False
>>> a[0].append(1)
>>> a                     # [[1]]
>>> b                     # [[1]]
>>> c                     # [[]]

Arguments des fonctions et référence

  • les paramètres de fonction sont passés par référence
  • entraîne l’aliasing des arguments
  • modification d’objets muables par effet de bord
>>> def f(l):
...    l.append(3)
...
>>> l1 = [1,2]
>>> f(l1)
>>> l1
[1,2,3]

Arguments et affectation

  • pourquoi les fonctions f ci-dessous ne modifient-t-elles pas leurs arguments?
>>> def f(x):
...    x = x+1
>>> x1 = 2
>>> f(x1) 
>>> x1
2
>>> def f(l):
...   l = [1]
>>> l1 = [1,2,3]
>>> f(l1)
>>> l1
[1,2,3]