Open In Colab

Mickaël Tits CETIC [email protected]

Chapitre 3 - Concepts avancés de programmation: exceptions, fonctions, objets

Gestion d'exceptions

Comme dans la plupart des langages de programmations, lorsqu'une instruction ne peut être correctement effectuée, l'interpréteur renvoie un message d'erreur, et s'arrête. On appelle ça une exception. Le message d'erreur est en général une source d'information importante pour le développeur, car il permet de comprendre le bug et ainsi de le résoudre.

On peut cependant contourner une exception pour permettre au programme de continuer, en définissant un comportement spécifique à adopter lorsqu'une exception survient.

In [0]:
#Exemple d'exception
x = 0
inverse = 1/x
  • On peut rediriger toute exception:
In [0]:
x = 0
try:
  inverse = 1/x
except:
  inverse = float("inf")  
print(inverse)
  • On peut vérifier quel type d'exception est survenue de cette manière:
In [0]:
try:
  inverse = 1/x
except Exception as e:
  print(e)
  inverse = float("inf")  
print(inverse)
  • On peut aussi gérer un type d'exception spécifique, pour modifier le comportement de façon pertinente selon le type d'exception.
  • Ca permet d'éviter de laisser passer un bug inattendu, ce qui rendrait le débogage plus compliqué
In [0]:
try:
  inverse = 1/y
except ZeroDivisionError as e:
  inverse = float("inf")
print(inverse)

Fonctions

  • Les fonctions permettent de rendre le code modulaire (on ne réécrit pas 50x la même chose)
  • On définit d'abord une fonction, avant de pouvoir l'appeler. Pour la définir:
    • on utilise le mot-clé "def"
    • on définit l'identifiant et les arguments de la fonctions (appelés signature de la fonction). E.g.: def my_function(x1, x2):

      my_function est l'identifiant de la fonction, et les éléments entre parenthèse x1, x2 sont les arguments de la fonction.

  • on écrit ensuite dans un bloc indenté d'instructions en-dessous: ce sont les instructions de la fonction. Généralement, ces instructions réalisent des traitements utilisant les variables passées en arguments (sinon les arguments ne servent à rien).
  • le mot-clé return permet de renvoyer un résultat de la fonction et de sortir de la fonction. Ce mot-clé peut être utilisé plusieurs fois (par exemple pour retourner un résultat différent selon une condition).

Exemple:

def my_function(x1, x2):
  out = x1*x2
  return out

On peut ensuite appeler une fonction de cette manière:

a = 6
b = 7
output = my_function(a, b)

Les variables a et b sont passées en arguments de la fonction my_function, et le résultat de la fonction (42) est renvoyé. La variable output est donc assignée à 42.

In [0]:
def factorial(n):
  
  """
  Cette fonction permet de calculer le factoriel de n
  """
  
  f = 1

  for i in range(1,n+1): 
    f = f * i 

  return f

print(factorial(2), factorial(3), factorial(20) )
  • Les variables définies dans une fonction n'existe que lors de l'exécution de cette fonction
  • Les traitements effectués sur les variables ne sont valables que durant l'éxécution de la fonction
In [0]:
x = 1
y = 2

def my_function():  
   
  print('x inside function =', x) #utilise la variable définie en-dehors de la fonction  

  try:
    print(y)
  except Exception as exc:
    print(exc)
  #print(y) #renvoie une erreur car une fonction locale du même nom est déclarée après  
  y = 4
  print('y inside function =', y) #imprime la variable locale
  
  z = 3 
  print('z inside function =', z) #imprime la variable locale
  

my_function()


print('x outside function =', x)
print('y outside function =', y)
try:
  print(z)
except Exception as exc:
  print(exc)
#print(z) # renvoie une erreur car la variable n'est pas définie en-dehors de la fonction
  • On peut définir plusieurs arguments
  • On peut définir des aguments par défaut
  • Lors de l'appel de la fonction, on peut assigner uniquement certains arguments en utilisant leur identifiant
In [0]:
def my_function(param1, param2 = "param2", param3 = "param3", param4 = "param4"):
  
    print(param1, param2, param3, param4)

try:
  my_function()
except Exception as e:
  print("Exception:", e)

my_function(1)
my_function(1, 2)
my_function(1, param3 = 2)

Objets

En Python, tout est objet

  • En Python, tout est objet, que ce soit les types de base, les collections d'objets (les conteneurs), et même les fonctions.
  • On peut par exemple créer une liste contenant un ensemble de fonctions (pour par exemple manipuler efficacement un pipeline d'opérations, ou pour regrouper ensemble des fonctions ad-hoc, des paramètres et des résutlats de ces fonctions).
In [0]:
def square(x):
  return x**2
def cube(x):
  return x**3
  
my_list = [square, cube]
operands = [4,5]
operations = [my_list, operands]

print(operations)

print( [[func(op) for func in operations[0]] for op in operations[1]])

Les objets immuables sont simples

Les types de base, tel que les int, float, string, bool sont immuables. Comme vu précédemment, les tuple sont également immuables. Immuable signifie qu'après leur création, on ne peut pas modifier les objets en mémoire. C'est pourquoi le code suivant donne une erreur :

In [0]:
my_tuple = (1,2,3)
my_tuple[0] = 42
In [0]:
my_int1 = 1
#On crée une nouvelle étiquette (référence) vers l'objet 1
my_int2 = my_int1
#la fonction id renvoie l'adresse mémoire de l'objet (i.e. des données réellement stockées dans la mémoire RAM de l'ordinateur!)
#Rien ne sert de stocker plusieurs fois l'entier 1 en mémoire, c'est pourquoi les deux variables renvoie au même endroit.
print( my_int1, my_int2)
print( id(my_int1) , id(my_int2) )

#L'objet entier est immuable, donc lorsqu'on veut modifier my_int2, l'objet 1 n'est pas modifié en mémoire. A la place, l'étiquette "my_int2" est réassignée vers un nouvel objet en mémoire (en l'occurrence un objet de type entier et de valeur égale à 2)
my_int2 += 1

print( my_int1, my_int2)
print( id(my_int1) , id(my_int2) )
In [0]:
#Les tuples sont plus faciles (et rapides) à assigner
%timeit my_tuple = (1,2,3, 4, True, "hello" )
%timeit my_list = [1,2,3, 4, True, "hello" ]
In [0]:
#Les tuples sont plus légers que les listes
import sys
my_tuple = (1,2,3, 4, True, "hello" )
my_list = list(my_tuple)
print(sys.getsizeof( my_tuple ) , sys.getsizeof( my_list ) )

Les objets mutables sont pratiques

A l'inverse, les list, dict, set sont mutables, c'est-à-dire qu'on peut modifier l'objet lui-même.

Attention: lorsqu'on assigne une variable avec un objet mutable existant, cette variable devient une référence vers le même objet en mémoire! Ainsi, le code ci-dessous va modifier à la fois les variables my_list1 et my_list2 :

In [0]:
my_list1 = [1,2,3]
my_list2 = my_list1
my_list2[0] = 42
my_list1.append(4)
my_list2 += [5]
print(my_list1, my_list2)
In [0]:
#L'opérateur "=" réassigne my_list2, ce qui crée donc une nouvelle liste (différente de my_list1)
my_list2 = my_list1+[6]
my_list2 += [7]
print(my_list1, my_list2)

En Python, les arguments sont passés par référence des objets

Lorsqu'on passe une variable en argument dans une fonction, c'est la référence de l'objet lui-même qui est passée à la fonction et non une copie (contrairement au langage C par exemple). ainsi, si l'objet passé en argument est mutable, celui-ci peut être modifié dans la fonction:

In [0]:
def modify(list_argument, int_argument):  
  list_argument+=[4]
  int_argument += 4
  print("inside:", list_argument, int_argument)
  
my_int = 2
my_list = [1,2,3]
modify(my_list, my_int)

#Un objet mutable est modifié en-dehors de la fonction, pas un objet immuable (puisque par définition ile ne peut être modifié)
print("outside:", my_list, my_int)

Attributs et méthodes

  • Chaque objet peut contenir des attributs, et des méthodes qui permettent de réaliser des instructions prenant en compte du contexte de l'objet (c'est-à-dire de ses attributs)
In [0]:
my_float = 4.2
help(my_float)
In [0]:
#Attributs d'un float
print(my_float.real, my_float.imag)
In [0]:
#Une méthode d'un float
my_float = 4.0
my_float.is_integer()
In [0]:
print(dir(my_list))
In [0]:
my_list.index(2, 5)

Les Classes

  • Les classes permettent de créer des nouveaux types d'objets, dont on peut définir les attributs et des méthodes. Les classes permettent d'encapsuler les différents attributs et méthodes dans une seule entité. L'encapsulation du code permet de rendre le code plus clair, et plus modulaire:
  • il est plus facile de modifier le comportement d'une classe sans impacter le reste du code
  • on peut facilement reprendre une classe et l'intégrer à un autre projet
  • on peut facilement ajouter des méthodes internes de vérification/validation/optimisation, transparentes pour le reste du programme.
  • on peut directement savoir quelles opératiosn peuvent être exécutées par un module (i.e. quels sont les "responsabilités/services offerts" par le module).
In [0]:
class House:
  
  """
  Cette classe permet de définir les propriétés d'une maison, donc l'adresse, le prix, la surface et le nombre de chambre.
  Elle permet de vérifier la validité desdonnées (validate()), d'afficher les informations sur la maison (display()), et de calculer le prix par m2 (compute_price_m2())
  """
  
  def __init__(self, address, price, surface, rooms):
    
    self.address = address
    self.price = price
    self.surface = surface
    self.rooms = rooms
    
    #On vérifie la validité des données, et on les rend valides si possible, sinon on renvoie False (pas valide)
    self.is_valid = self.validate()
    
  def compute_price_m2(self):
        
    return self.price/self.surface
  
  def validate(self):
    
    """
    Vérifie si l'objet est valide
    """    
    
    #Si le prix, la surface, ou le nombre de chambre ne sont pas des int, on essaye de les convertir, sinon pas valide
    try:
      self.price = int(self.price)
      self.surface = int(self.surface)
      self.rooms = int(self.rooms)
      return True
    except:
      return False
  
  def display(self):
    
    if self.is_valid:
      print(self.address, ":", self.price, "€, ", self.surface, "m2", self.rooms, "rooms")
    else:
      print("L'élément n'est pas un élément valide:", self.address)
     
In [0]:
house1 = House("Rue de Bruxelles 42, Namur", 300000, 120, 3)
house1.display()
#appeler un attribut (pas de parenthèses)
print(house1.price)
#appeler une méthode (parenthèses nécessaires)
print(house1.compute_price_m2())

Remarque: On pourrait définir chaque méthode comme une fonction en-dehors du cadre de la classe, mais ça n'a pas d'intérêt puisque le processus est spécifique à la classe House. De plus, ça rend le code moins clair et rend possible une utilisation non-conforme, ce qui amène à des bugs:

In [0]:
def display(my_house):

  if my_house.is_valid:
    print(my_house.address, ":", my_house.price, "€, ", my_house.surface, "m2", my_house.rooms, "rooms")
  else:
    print("L'élément n'est pas un élément valide:", my_house.address)

display(house1)
In [0]:
display([1,2,3])
In [0]:
#Attention, par défaut les classes sont mutables
house2 = house1
house2.price = 350000
print(house1.price)
In [0]:
#pour créer une copie d'un objet mutable, on peut utiliser un module Python: copy.copy
from copy import copy

house2 = copy(house1)

house2.price = 500000
print(house1.price)

Remarque: On peut tout à fait ajouter un attribut à un objet créé à partir d'une classe. Ce n'est cependant pas conseillé car les objets d'une même classe perdent alors leur homogénéité.

In [0]:
house2.floors = 2
print(house2.floors)
In [0]:
#L'hétérogénéité des objets d'une même classe rendent le programme plus compliqué, diminuent la clarté et augmentent les risques de bugs
print(house1.floors)

Maintenant que vous connaissez les concepts principaux du langage Python, vous pouvez passez au Chapitre 4: Un exemple concret: analysons quelques biens immobiliers...