Introduction pas à pas de Python [version étendue]

Ce notebook constitue une rapide introduction à Python. Le langage est découvert en exécutant un grand nombre de simples et courtes lignes de code. L’objectif est d’entrapercevoir la manière dont Python fonctionne afin d’éviter de tomber dans les pièges classiques des débutants.

Cette approche est inspirée des tutoriels Python by immersion et Python Epiphanies créés par Stuart Williams.

Indentation


Tout est dans le style


Python a été conçu de manière à être facile à lire, partant du principe qu’on passe plus de temps à lire du code qu’à en écrire. Cela se matérialise par l’importance de l”indentation (décalage d’une ligne de code vers la droite). L’indentation du code détermine en partie sa structure. Dans l’exemple ci-dessous, les deux instructions print sont situées au même niveau d’indentation.

[1]:
print("Hey")
print("you")
Hey
you

Le code est exécuté normalement. La deuxième instruction du code suivant a été décalée de deux espaces, le code ne peut tout simplement plus être exécuté car cette structure n’a aucun sens pour Python.

[2]:
print("Hey")
  print("you")
  File "<ipython-input-2-a6cbeba02af8>", line 2
    print("you")
    ^
IndentationError: unexpected indent

L’utilité de l’indentation est plus évidente dans le cas de blocs logiques, tel qu’un bloc for. Le code ci-dessous s’exécute correctement.

[3]:
for i in [1, 2, 3]:
    print(i)  # L'indentation est de 4 espaces en général.
1
2
3

Alors que le code ci-dessous ne peut pas être exécuté, les instructions sous la ligne for n’étant pas indentées. Comme on peut le voir, c’est le code du dessus qui est le plus facile à lire, l’indentation nous aide à comprendre la structure logique du code.

[4]:
for i in [1, 2, 3]:
print(i)
  File "<ipython-input-4-3e96090b8527>", line 2
    print(i)
        ^
IndentationError: expected an indented block

Afin de définir les boucles et autres structures de contrôle, d’autres langages utilisent des symbôles ({}, ()). En choisissant d’utiliser l’indentation au lieu de symboles, Python a faix le choix du code épuré.

Commentaires et pass


Fainéant


Les commentaires sont des lignes qui débutent par un signe #. Ces lignes-là sont ignorées par l’interpréteur.

[5]:
# Ceci est un commentaire.
[6]:
# On
# peut
# les
# enchaîner
[7]:
###### Ceci aussi est un commentaire.

On peut rajouter un commentaire à la suite d’un code valide (séparé de deux espaces par convention).

[8]:
3.14  # Ceci est aussi un commentaire.
[8]:
3.14

Le keyword pass fait comme son nom l’indique, il passe son tour.

[9]:
pass

Objets


Tout est un objet!


Exécuter les instructions simples ci-dessous peut faire penser que Python ne fait rien de plus qu’une simple calculette.

[10]:
3.4
[10]:
3.4
[11]:
3.4 + 4
[11]:
7.4

Pourtant, beaucoup de choses se passent!

[12]:
dir(3.4)
[12]:
['__abs__',
 '__add__',
 '__bool__',
 '__class__',
 '__delattr__',
 '__dir__',
 '__divmod__',
 '__doc__',
 '__eq__',
 '__float__',
 '__floordiv__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getformat__',
 '__getnewargs__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__int__',
 '__le__',
 '__lt__',
 '__mod__',
 '__mul__',
 '__ne__',
 '__neg__',
 '__new__',
 '__pos__',
 '__pow__',
 '__radd__',
 '__rdivmod__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__rfloordiv__',
 '__rmod__',
 '__rmul__',
 '__round__',
 '__rpow__',
 '__rsub__',
 '__rtruediv__',
 '__set_format__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__sub__',
 '__subclasshook__',
 '__truediv__',
 '__trunc__',
 'as_integer_ratio',
 'conjugate',
 'fromhex',
 'hex',
 'imag',
 'is_integer',
 'real']

La fonction dir retourne une liste de tous les attributs d’un objet. La liste ci-dessus montre donc tous les attributs de l’objet 3.4. Les derniers attributs affichés, dont is_integer, nous indiquent que Python a compris que 3.4 est un nombre. On peut le vérifier en inspectant la valeur de l’attribut __class__. Pour accéder à la valeur d’un attribut, on rajout un point ``.`` après l’objet suivi du nom de l’attribut.

[13]:
3.4.__class__
[13]:
float

float est un type d’objet qui représente les nombres réels. Mais plutôt que de vérifier le type d’un objet en accédant à son attribut *__class__*, on peut utiliser la fonction type. Cette fonction, comme dir, fait partie des fonctions dites built-in car faisant parties intégrantes de Python (il n’y a pas besoin d’importer quoi que ce soit pour les appeler). On va voir plusieurs de ces fonctions dans la suite du notebook.

[14]:
type(3.4)
[14]:
float

On a donc vu que lorsqu’on exécute 3.4, Python ne fait pas juste qu’afficher ce nombre mais l’interprète comme un objet de type float doté de nombreux attributs. Que se passe-t-il alors lorsqu’on exécute 3.4 + 4?

[15]:
3.4.__add__(4)
[15]:
7.4

L’addition que nous avons écrite est en fait exécutée par l’instruction ci-dessus. Python voit que nous avons écrit un + après 3.4, il cherche alors l’attribut dunder (racourci de double underscore donné aux attributs qui débutent et terminent par __) __add__ dans la liste des attributs de 3.4. L’attribut __add__ des floats est en fait une méthode qui est capable de faire une action, dans ce cas-là, une addition. Lorsque Python trouve l’attribut __add__ de 3.4, il l’appelle avec l”argument 4. La valeur retournée est le résultat de l’addition.

Python n’est donc évidemment pas qu’une simple calculatrice. Lorsqu’il évalue les expressions qu’on lui donne, il crée des objets dotés d’un type et d’attributs. Ces attributs sont soit équivalent à des propriétés, comme __class__, soit des méthodes, comme __add__. Cette mécanique est présente partout dans le langage, celui-ci travaille en effet beaucoup pour nous. Il faut aussi noter que Python s’occupe d’allouer et libérer la mémoire à notre place.

[16]:
type(3.4.__add__)
[16]:
method-wrapper

Chaque objet créé a sa propre identité définie comme son adresse physique dans la mémoire.

[17]:
id(3.4)
[17]:
2556173076280
[18]:
id(3.4)
[18]:
2556173076352
[19]:
id(4)
[19]:
140734432777120

dir et id sont des fonctions qu’on n’utilise jamais dans un script mais qui sont très utiles pour apprendre Python et inspecter des objets.

[20]:
type(dir)
[20]:
builtin_function_or_method
[21]:
type(id)
[21]:
builtin_function_or_method

Une fonction est aussi un objet (tout est un objet!) avec ses propres attributs, un type et une identité.

[22]:
dir(dir)
[22]:
['__call__',
 '__class__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__name__',
 '__ne__',
 '__new__',
 '__qualname__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__self__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__text_signature__']
[23]:
id(dir)
[23]:
2556130333032
[24]:
id(dir)
[24]:
2556130333032

Erreurs


Antifragilité


Lorsqu’on écrit du code en Python et qu’on l’exécute pour la première fois, il est habituel de voir un message d’erreur apparaître. Ces erreurs nous indiquent ce qui ne fonctionne pas dans le code, et nous oriente pour résoudre le bug qui a causé l’erreur. Python dispose d’un grand nombre de genres d’erreur, à force de les déclencher on s’habitue à les reconnaître et on comprend de plus en plus rapidement d’où vient le problème.

[25]:
type(1)
 type(2)  # L'espace déclenche l'erreur
  File "<ipython-input-25-f456bc8300ff>", line 2
    type(2)  # L'espace déclenche l'erreur
    ^
IndentationError: unexpected indent

[26]:
1
3 +)°^ 3  # Cette suite de symboles n'a aucun sens
  File "<ipython-input-26-57174e9a2a28>", line 2
    3 +)°^ 3  # Cette suite de symboles n'a aucun sens
       ^
SyntaxError: invalid syntax

Les erreurs IntendationError et SyntaxError nous indique que le code n’a pas été écrit correctement. L’interpréteur analyse tout le code qu’on a écrit, s’il détecte ce genre d’erreur, il nous l’indique et n’exécute aucune ligne de code. Toutes (?) les autres erreurs qu’on peut avoir surviennent pendant l’exécution du code (runtime).

Keywords


Un peu de liant


Avec l’indentation, les keywords structurent le langage. On utilisera principalement les suivants:

  • Le vide: None
  • Booléens: True, False
  • Opérations booléennes: and, or, not
  • Appartenance: in, not in
  • Identification: is, is not
  • Boucles: for, while break, continue, pass,
  • Conditions: if, elif, else
  • Fonctions: def, return
  • Import: import, from, as
  • Déférencement: del
  • Pas d’action: pass
[35]:
help("keywords")

Here is a list of the Python keywords.  Enter any keyword to get more help.

False               class               from                or
None                continue            global              pass
True                def                 if                  raise
and                 del                 import              return
as                  elif                in                  try
assert              else                is                  while
async               except              lambda              with
await               finally             nonlocal            yield
break               for                 not

Les keywords sont des mots réservés, aucun objet ne peut avoir un nom qui fait partie des keywords.

[36]:
True = 2
  File "<ipython-input-36-da3e5402d318>", line 1
    True = 2
            ^
SyntaxError: can't assign to keyword

[37]:
true = 2  # On peut faire ça, mais c'est maladroit!
[38]:
and = 3
  File "<ipython-input-38-f117365eecb9>", line 1
    and = 3
      ^
SyntaxError: invalid syntax

[39]:
AND = 3  # Toujours autant maladroit!
[40]:
help(for)
  File "<ipython-input-40-c96d1524574d>", line 1
    help(for)
           ^
SyntaxError: invalid syntax

[41]:
help("for")
The "for" statement
*******************

The "for" statement is used to iterate over the elements of a sequence
(such as a string, tuple or list) or other iterable object:

   for_stmt ::= "for" target_list "in" expression_list ":" suite
                ["else" ":" suite]

The expression list is evaluated once; it should yield an iterable
object.  An iterator is created for the result of the
"expression_list".  The suite is then executed once for each item
provided by the iterator, in the order returned by the iterator.  Each
item in turn is assigned to the target list using the standard rules
for assignments (see Assignment statements), and then the suite is
executed.  When the items are exhausted (which is immediately when the
sequence is empty or an iterator raises a "StopIteration" exception),
the suite in the "else" clause, if present, is executed, and the loop
terminates.

A "break" statement executed in the first suite terminates the loop
without executing the "else" clause’s suite.  A "continue" statement
executed in the first suite skips the rest of the suite and continues
with the next item, or with the "else" clause if there is no next
item.

The for-loop makes assignments to the variables(s) in the target list.
This overwrites all previous assignments to those variables including
those made in the suite of the for-loop:

   for i in range(10):
       print(i)
       i = 5             # this will not affect the for-loop
                         # because i will be overwritten with the next
                         # index in the range

Names in the target list are not deleted when the loop is finished,
but if the sequence is empty, they will not have been assigned to at
all by the loop.  Hint: the built-in function "range()" returns an
iterator of integers suitable to emulate the effect of Pascal’s "for i
:= a to b do"; e.g., "list(range(3))" returns the list "[0, 1, 2]".

Note: There is a subtlety when the sequence is being modified by the
  loop (this can only occur for mutable sequences, e.g. lists).  An
  internal counter is used to keep track of which item is used next,
  and this is incremented on each iteration.  When this counter has
  reached the length of the sequence the loop terminates.  This means
  that if the suite deletes the current (or a previous) item from the
  sequence, the next item will be skipped (since it gets the index of
  the current item which has already been treated).  Likewise, if the
  suite inserts an item in the sequence before the current item, the
  current item will be treated again the next time through the loop.
  This can lead to nasty bugs that can be avoided by making a
  temporary copy using a slice of the whole sequence, e.g.,

     for x in a[:]:
         if x < 0: a.remove(x)

Related help topics: break, continue, while

A part True, False et None, les keywords ne sont pas des objets.

[42]:
type(and)
  File "<ipython-input-42-5d36ea62b089>", line 1
    type(and)
           ^
SyntaxError: invalid syntax

[43]:
print(type(True), type(False), type(None))
<class 'bool'> <class 'bool'> <class 'NoneType'>

Opérateurs principaux


Pas que pour les maths!


[44]:
3.4 + 4
[44]:
7.4
[45]:
3.4 - 4
[45]:
-0.6000000000000001
[46]:
3.4 * 4
[46]:
13.6
[47]:
3.4 / 4
[47]:
0.85
[48]:
11 // 4
[48]:
2
[49]:
(11).__floordiv__(4)
[49]:
2
[50]:
11 % 4
[50]:
3
[51]:
(11 // 4) * 4 + (11 % 4)
[51]:
11
[52]:
11 // 4 * 4 + 11 % 4  # Moins facile à lire
[52]:
11
[53]:
3.4 > 4
[53]:
False
[54]:
3.4 < 4
[54]:
True
[55]:
3.4 <= 4
[55]:
True
[56]:
3.4 >= 4
[56]:
False
[57]:
2 < 5 < 10
[57]:
True
[58]:
3 > 2 >= 10
[58]:
False
[59]:
3.4 == 4
[59]:
False
[60]:
3.4.__eq__(4)
[60]:
False
[61]:
3.4 != 4
[61]:
True
[62]:
4 == 4
[62]:
True

Les symboles opérateurs ne sont pas des objets.

[63]:
type(==)
  File "<ipython-input-63-eab1840bb5e3>", line 1
    type(==)
          ^
SyntaxError: invalid syntax

Nommer


Poser des post-its par-ci, par-là


Jusqu’à présent les objets qui ont été utilisés sont supprimés aussitôt générés par Python. On peut les garder à notre disposition en leur assignant un nom.

[64]:
height = 1.73
[65]:
height
[65]:
1.73
[66]:
type(height)
[66]:
float
[67]:
id(height)
[67]:
2556173077552
[68]:
id(height)
[68]:
2556173077552
[69]:
print(height, height, height)
1.73 1.73 1.73

Regardez bien les trois instructions ci-dessous, quelle est la valeur finale de ``x``?

[70]:
x = 2
y = x
y = 4
[71]:
print(x, y)
2 4

On aurait pu croire que x valait 4 à la fin des trois instructions, mais non. Les noms en Python sont juste des références aux objets, on peut les comparer à des post-its.

[72]:
x = 2
# L'objet 2 est créé, un post-it avec inscrit *x* lui est aposé.
print("x =", x, " / id(x):", id(x))
y = x
# Un second post-it est aposé sur l'objet 2, il y est inscrit *y*.
print("x =", x, " / id(x):", id(x), "  ---  y =", y, " / id(y):", id(y))
# Aucun objet n'est créé lors de cette instruction.
y = 4
# L'objet 4 est créé, comme un nom ne peut référé qu'à un seul objet
# le post-it *y* qui était sur l'objet 2 est supprimé et un nouveau
# post-it *y* est aposé sur l'objet 4.
print("x =", x, " / id(x):", id(x), "  ---  y =", y, " / id(y):", id(y))
x = 2  / id(x): 140734432777056
x = 2  / id(x): 140734432777056   ---  y = 2  / id(y): 140734432777056
x = 2  / id(x): 140734432777056   ---  y = 4  / id(y): 140734432777120

Une fois que des références aux objets ont été créées, celles-ci sont rajoutées au namespace. On peut en voir le contenu avec la fonction dir.

[73]:
print(dir())
['AND', 'In', 'Out', '_', '_10', '_11', '_12', '_13', '_14', '_15', '_16', '_17', '_18', '_19', '_20', '_21', '_22', '_23', '_24', '_29', '_30', '_44', '_45', '_46', '_47', '_48', '_49', '_50', '_51', '_52', '_53', '_54', '_55', '_56', '_57', '_58', '_59', '_60', '_61', '_62', '_65', '_66', '_67', '_68', '_8', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '_dh', '_i', '_i1', '_i10', '_i11', '_i12', '_i13', '_i14', '_i15', '_i16', '_i17', '_i18', '_i19', '_i2', '_i20', '_i21', '_i22', '_i23', '_i24', '_i25', '_i26', '_i27', '_i28', '_i29', '_i3', '_i30', '_i31', '_i32', '_i33', '_i34', '_i35', '_i36', '_i37', '_i38', '_i39', '_i4', '_i40', '_i41', '_i42', '_i43', '_i44', '_i45', '_i46', '_i47', '_i48', '_i49', '_i5', '_i50', '_i51', '_i52', '_i53', '_i54', '_i55', '_i56', '_i57', '_i58', '_i59', '_i6', '_i60', '_i61', '_i62', '_i63', '_i64', '_i65', '_i66', '_i67', '_i68', '_i69', '_i7', '_i70', '_i71', '_i72', '_i73', '_i8', '_i9', '_ih', '_ii', '_iii', '_oh', 'exit', 'get_ipython', 'height', 'i', 'quit', 'true', 'x', 'y']

Dans une console IPython ou dans un notebook, le namespace est occupé par beaucoup de références qui ne nous intéressent pas. Les commandes magiques %who et whos permettent de voir uniquement celles que nous avons créées nous-mêmes.

[74]:
%who
AND      height  i       true    x       y
[75]:
%whos
Variable   Type     Data/Info
-----------------------------
AND        int      3
height     float    1.73
i          int      3
true       int      2
x          int      2
y          int      4

Ainsi, lorsqu’on l’on fait référence à un objet par un de ses noms, Python cherche dans le namespace la référence de cet objet et utilise la donnée qui lui est associée.

[76]:
height
[76]:
1.73
[77]:
poids
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-77-adb260374773> in <module>
----> 1 poids

NameError: name 'poids' is not defined

Comme poids n’est pas une référence du namespace (on peut aussi dire que la variable poids n’a pas encore été définie), l’interpréteur retourne une erreur.

On peut déréférencer un élément du namespace avec une instruction utilisant le keyword del.

[78]:
# On enlève le post-it *height* de l'objet 1.73,
# celui-ci n'est plus accessible à la suite de ça.
del height
[79]:
%who
AND      i       true    x       y
[80]:
del height
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-80-f8c467be9fdb> in <module>
----> 1 del height

NameError: name 'height' is not defined
[81]:
height
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-81-5107fc8585e9> in <module>
----> 1 height

NameError: name 'height' is not defined

On peut déférencer tous les éléments du namespace avec la commande magique %reset -f.

[82]:
%reset -f
[83]:
%whos
Interactive namespace is empty.

Importer


Batteries included


Nous avons pour l’instant vu que nous avions à disposition des fonctions built-in, des keywords et des opérateurs. Un mécanisme permet d”importer des objets supplémentaires dans le code, comme des fonctions.

[84]:
import math

math est un module de la standard library. La standard library est une grande collection de modules livrés avec Python, c’est pourquoi on associe souvent l’expression Batteries Included au langage.

[85]:
%whos
Variable   Type      Data/Info
------------------------------
math       module    <module 'math' (built-in)>

math est le module dédié aux opérations mathématiques. En exécutant import math, on a rajouté au namespace le nom math, celui-ci faisant référence à un objet de type module.

[86]:
type(math)
[86]:
module
[87]:
id(math)
[87]:
2556140748616
[88]:
print(dir(math))
['__doc__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh', 'degrees', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'pi', 'pow', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc']

On peut voir que le module contient les attributs sqrt (racine carrée) et pi. On accède à ces attributs en rajoutant un . après math.

[89]:
math.sqrt
[89]:
<function math.sqrt(x, /)>
[90]:
math.pi
[90]:
3.141592653589793
[91]:
type(math.sqrt)
[91]:
builtin_function_or_method
[92]:
type(math.pi)
[92]:
float
[93]:
math.sqrt(math.pi)
[93]:
1.7724538509055159

On peut importer un module et les objets qu’il contient de plusieurs manières:

  • from math import * importe tout le contenu du module math. Cette méthode n’est pas recommandée car elle vient rajouter beaucoup de références au namespace.
[94]:
%reset -f
[95]:
from math import *
[96]:
%who
acos     acosh   asin    asinh   atan    atan2   atanh   ceil    copysign
cos      cosh    degrees         e       erf     erfc    exp     expm1   fabs
factorial        floor   fmod    frexp   fsum    gamma   gcd     hypot   inf
isclose  isfinite        isinf   isnan   ldexp   lgamma  log     log10   log1p
log2     modf    nan     pi      pow     radians         remainder       sin     sinh
sqrt     tan     tanh    tau     trunc
[97]:
sqrt(pi)
[97]:
1.7724538509055159
  • import math as m importe le module math avec pour référence m et non math. Cette méthode peut être utilisé lorsque le nom du module est très long, ou si ce nom va être répété à de très nombreuses reprises. Trois examples classiques étant import numpy as np, import pandas as pd, import matplotlib.pyplot as plt.
[98]:
%reset -f
[99]:
import math as m
[100]:
math
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-100-a984292e3e46> in <module>
----> 1 math

NameError: name 'math' is not defined
[101]:
m
[101]:
<module 'math' (built-in)>
[102]:
%whos
Variable   Type      Data/Info
------------------------------
m          module    <module 'math' (built-in)>
[103]:
m.sqrt(m.pi)
[103]:
1.7724538509055159
  • from math import sqrt, pi importe seulement les objets sqrt et pi.
[104]:
%reset -f
[105]:
from math import sqrt, pi
[106]:
%whos
Variable   Type                          Data/Info
--------------------------------------------------
pi         float                         3.141592653589793
sqrt       builtin_function_or_method    <built-in function sqrt>
[107]:
sqrt(pi)
[107]:
1.7724538509055159
  • from math import sqrt as racinecarree importe directement la fonction sqrt du module math sous la référence racinecarree.
[108]:
%reset -f
[109]:
from math import sqrt as racinecarree
[110]:
%whos
Variable       Type                          Data/Info
------------------------------------------------------
racinecarree   builtin_function_or_method    <built-in function sqrt>
[111]:
racinecarree(4)
[111]:
2.0
[112]:
sqrt(4)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-112-317e033d29d5> in <module>
----> 1 sqrt(4)

NameError: name 'sqrt' is not defined

Des méthodes d’import montrées ci-dessus, c’est la première, ``import math``, qui doit être préférée. Elle permet en effet de savoir depuis quel module proviennent les fonctions qu’on utilise.

[113]:
%reset -f

Modules et packages


Batteries included in batteries


Les modules sont matérialisés par des fichiers textes dont l’extension est ``.py``. Lorsqu’on importe un module, le code qu’il contient est exécuté. Les objets qui sont créés dans ce module sont donc accessibles.

[114]:
%%writefile dummymodule.py
"""Un tout petit module."""
print("hello world")
x = 2
Writing dummymodule.py
[115]:
import dummymodule
hello world
[116]:
dummymodule.x
[116]:
2

!!! Attention !!! Importer un module déjà importé ne réexécute pas son code.

[117]:
import dummymodule
[118]:
dummymodule.x
[118]:
2

Si on modifie le code d’un module qu’on utilise pendant qu’on écrit un script, il faut penser à forcer son import. On peut utiliser le module importlib pour importer à chaque fois le module modifié.

[119]:
import importlib
importlib.reload(dummymodule)
hello world
[119]:
<module 'dummymodule' from 'D:\\GoogleDrive\\Code\\IntroPython\\source\\dummymodule.py'>

Les modules sont aussi des objets (tout est un objet).

[120]:
type(dummymodule)
[120]:
module
[121]:
dir(dummymodule)
[121]:
['__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'x']
[122]:
dummymodule.__name__
[122]:
'dummymodule'
[123]:
dummymodule.__file__
[123]:
'D:\\GoogleDrive\\Code\\IntroPython\\source\\dummymodule.py'
[124]:
dummymodule.__doc__
[124]:
'Un tout petit module.'
[125]:
help(dummymodule)
Help on module dummymodule:

NAME
    dummymodule - Un tout petit module.

DATA
    x = 2

FILE
    d:\googledrive\code\intropython\source\dummymodule.py


[126]:
dummymodule?

Lorsqu’il tente d’importer quelque chose, Python cherche dans une liste de dossier qu’on retrouve dans la référence sys.path. Cette liste contient le dossier courant.

[127]:
import sys
sys.path
[127]:
['D:\\GoogleDrive\\Code\\IntroPython\\source',
 'C:\\Users\\maxim\\Miniconda3\\python37.zip',
 'C:\\Users\\maxim\\Miniconda3\\DLLs',
 'C:\\Users\\maxim\\Miniconda3\\lib',
 'C:\\Users\\maxim\\Miniconda3',
 '',
 'C:\\Users\\maxim\\AppData\\Roaming\\Python\\Python37\\site-packages',
 'C:\\Users\\maxim\\Miniconda3\\lib\\site-packages',
 'C:\\Users\\maxim\\Miniconda3\\lib\\site-packages\\win32',
 'C:\\Users\\maxim\\Miniconda3\\lib\\site-packages\\win32\\lib',
 'C:\\Users\\maxim\\Miniconda3\\lib\\site-packages\\Pythonwin',
 'C:\\Users\\maxim\\Miniconda3\\lib\\site-packages\\IPython\\extensions',
 'C:\\Users\\maxim\\.ipython']

Les modules de la standard library écrits en Python (il y a aussi des modules écrits en C) sont visibles sur GitHub. Dans ce dossier on trouve aussi des sous-dossiers qui contiennent des fichiers .py et un fichier __init__.py. Ces dossiers-là sont des packages qu’on peut aussi importer.

[128]:
import email
[129]:
type(email)
[129]:
module
[130]:
dir(email)
[130]:
['__all__',
 '__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__path__',
 '__spec__',
 '_encoded_words',
 '_parseaddr',
 '_policybase',
 'base64mime',
 'charset',
 'encoders',
 'errors',
 'feedparser',
 'header',
 'iterators',
 'message',
 'message_from_binary_file',
 'message_from_bytes',
 'message_from_file',
 'message_from_string',
 'parser',
 'quoprimime',
 'utils']
[131]:
email.__package__
[131]:
'email'
[132]:
email.__path__
[132]:
['C:\\Users\\maxim\\Miniconda3\\lib\\email']
[133]:
email.__file__
[133]:
'C:\\Users\\maxim\\Miniconda3\\lib\\email\\__init__.py'
[134]:
email.__doc__
[134]:
'A package for parsing, handling, and generating email messages.'
[135]:
help(email)
Help on package email:

NAME
    email - A package for parsing, handling, and generating email messages.

PACKAGE CONTENTS
    _encoded_words
    _header_value_parser
    _parseaddr
    _policybase
    base64mime
    charset
    contentmanager
    encoders
    errors
    feedparser
    generator
    header
    headerregistry
    iterators
    message
    mime (package)
    parser
    policy
    quoprimime
    utils

FUNCTIONS
    message_from_binary_file(fp, *args, **kws)
        Read a binary file and parse its contents into a Message object model.

        Optional _class and strict are passed to the Parser constructor.

    message_from_bytes(s, *args, **kws)
        Parse a bytes string into a Message object model.

        Optional _class and strict are passed to the Parser constructor.

    message_from_file(fp, *args, **kws)
        Read a file and parse its contents into a Message object model.

        Optional _class and strict are passed to the Parser constructor.

    message_from_string(s, *args, **kws)
        Parse a string into a Message object model.

        Optional _class and strict are passed to the Parser constructor.

DATA
    __all__ = ['base64mime', 'charset', 'encoders', 'errors', 'feedparser'...

FILE
    c:\users\maxim\miniconda3\lib\email\__init__.py


[136]:
import email.message
[137]:
type(email.message)
[137]:
module
[138]:
email.message.__file__
[138]:
'C:\\Users\\maxim\\Miniconda3\\lib\\email\\message.py'
[139]:
email.message.__package__
[139]:
'email'
[140]:
import email.mime
[141]:
email.mime.__package__
[141]:
'email.mime'
[142]:
dir(email.mime)
[142]:
['__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__path__',
 '__spec__']
[143]:
help(email.mime)
Help on package email.mime in email:

NAME
    email.mime

PACKAGE CONTENTS
    application
    audio
    base
    image
    message
    multipart
    nonmultipart
    text

FILE
    c:\users\maxim\miniconda3\lib\email\mime\__init__.py


[144]:
from email.mime import audio
[145]:
type(audio)
[145]:
module
[146]:
audio.__file__
[146]:
'C:\\Users\\maxim\\Miniconda3\\lib\\email\\mime\\audio.py'

Voici une très courte liste de modules utiles.

[147]:
import sys
sys.executable
[147]:
'C:\\Users\\maxim\\Miniconda3\\python.exe'
[148]:
sys.version
[148]:
'3.7.3 (default, Apr 24 2019, 15:29:51) [MSC v.1915 64 bit (AMD64)]'
[149]:
import os
os.getcwd()
[149]:
'D:\\GoogleDrive\\Code\\IntroPython\\source'
[150]:
import time
t1 = time.time()
time.sleep(2)
t2 = time.time()
print(t2 - t1)
2.0006837844848633
[151]:
import datetime
guess = datetime.datetime(2019, 9, 10, 10, 5, 14)
now = datetime.datetime.now()
diff = now - guess
print("Je me suis trompé de", diff.seconds, "secondes.")
Je me suis trompé de 48127 secondes.
[152]:
import random
random.random()
[152]:
0.1681579307440323
[153]:
random.randint(10, 20)
[153]:
15
[154]:
random.choice(["a", "b", "c"])
[154]:
'a'
[155]:
random.sample([1, 2, 3, 4], 3)
[155]:
[1, 3, 2]
[156]:
random.gauss(10, 3)
[156]:
7.155043539141076
[157]:
import pathlib
current_directory = pathlib.Path.cwd()
current_directory
[157]:
WindowsPath('D:/GoogleDrive/Code/IntroPython/source')
[158]:
list(current_directory.glob("*"))
[158]:
[WindowsPath('D:/GoogleDrive/Code/IntroPython/source/.ipynb_checkpoints'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/conf.py'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/dummymodule.py'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/environnement.rst'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/exercices.rst'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/images'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/index.rst'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/installation.rst'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/introduction.rst'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/intropython.ipynb'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/intropython.py'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/intropython_extended.ipynb'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/python.py'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/python.rst'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/rstfiles.txt'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/scientific_libs.ipynb'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/scientific_libs.py'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/text.txt'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/TODOs.txt'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/utilisation.rst'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/_build'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/_static'),
 WindowsPath('D:/GoogleDrive/Code/IntroPython/source/__pycache__')]
[159]:
import collections
collections.Counter("aaabbbbbbbbbbbbbcccddddee").most_common()
[159]:
[('b', 13), ('d', 4), ('a', 3), ('c', 3), ('e', 2)]
[160]:
%reset -f
!del /f dummymodule.py

Conditions if et booléens


Avec des si


Le bloc sous une instruction if est exécuté entièrement si la condition (expression) qui suit le if est évaluée comme vraie.

[161]:
if True:
    print("Executed")
Executed
[162]:
if False:
    print("Not executed")
[163]:
diameter = 2
if diameter > 1:
    print(diameter)
2

On peut rajouter d’autres conditions avec les keywords elif et else.

[164]:
age = 150
if age < 18:
    print("Sorry you're not allowed to drive.")
elif 18 <= age < 80:
    print("Let's drive!")
elif 80 <= age < 122:
    print("Please please please strop driving please!")
else:
    print("Is that you Jeanne?")
Is that you Jeanne?

On peut assigner le résultat d’un comparaison à une référence et on peut combiner plusieurs comparaisons avec les keywords and et or?

[165]:
age1 = 10
age2 = 54
condition1 = 18 <= age1 < 80
condition2 = 18 <= age2 < 80
condition1 and condition2
[165]:
False
[166]:
True and True
[166]:
True
[167]:
True or True
[167]:
True
[168]:
False and False
[168]:
False
[169]:
False or False
[169]:
False
[170]:
False and True
[170]:
False
[171]:
True and False
[171]:
False
[172]:
False or True
[172]:
True
[173]:
True or False
[173]:
True
[174]:
print(type(condition1), type(condition2))
<class 'bool'> <class 'bool'>

True et False sont des booléens. Dans les versions précédentes, 1 était utilisé pour représenter ce qui était vrai, et 0 pour ce qui était faux. On hérite de ce comportement aujourd’hui.

[175]:
True + 3
[175]:
4
[176]:
False + False
[176]:
0

Tous les objets de Python sont en fait soit vrai ou faux. On peut vérifier s’ils sont truthy ou falsy avec la fonction bool.

[177]:
bool(3.14)
[177]:
True
[178]:
if 3.14:
    print("3.14")
3.14
[179]:
bool(0)
[179]:
False
[180]:
if 0:
    print("0")
[181]:
bool(1)
[181]:
True
[182]:
if 1:
    print("1")
1
[183]:
bool(print)
[183]:
True
[184]:
if print:
    print("Never do that please!")
Never do that please!
[185]:
bool(None)
[185]:
False
[186]:
if not None:
    print("Pas vide")
Pas vide
[187]:
%reset -f

Texte


Du blabla


Le texte est représenté par des objets de type string. On encadre le texte des symboles " ou ' ou """ ou ''' pour créer un objet string.

[188]:
"Bob"
[188]:
'Bob'
[189]:
'Bob'
[189]:
'Bob'
[190]:
"""Bob"""
[190]:
'Bob'
[191]:
'''Bob'''
[191]:
'Bob'
[192]:
type("Bob")
[192]:
str
[193]:
print("'")
'
[194]:
print('"')
"

On peut les additionner et les démultiplitier.

[195]:
"Bob" + "Bill"
[195]:
'BobBill'
[196]:
"Bob" * 3
[196]:
'BobBobBob'
[197]:
3 * "Bob"
[197]:
'BobBobBob'
[198]:
"Bob" - "Bill"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-198-a8c1ff178148> in <module>
----> 1 "Bob" - "Bill"

TypeError: unsupported operand type(s) for -: 'str' and 'str'
[199]:
"Bob" / 3
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-199-f7f9e058c01b> in <module>
----> 1 "Bob" / 3

TypeError: unsupported operand type(s) for /: 'str' and 'int'

Il est possible de créer une string vide.

[200]:
""
[200]:
''
[201]:
"Bob" + "" + "Bill"
[201]:
'BobBill'

Le caractère backslash ```` déclenche un comportement spécial: l’interpréteur regarde le caractère qui suit et voit si la combinaison des deux caractères forme ou non un caractère spécial, si oui, celui-ci est représenté.

[202]:
# \n --> Retour à la ligne
# \t --> Tabulation

print("Bob\tBill\nBill\tBob")
Bob     Bill
Bill    Bob

Le caractère \ a donc un effet particulier. Cela rend plus difficile la représentation d’un chemin vers un dossier ou un fichier sous Windows. On annule l’effet particulier de \ en rajoutant le préfixe r (pour raw) devant le premier guillemet de la string.

[203]:
print(r"user\me\names.txt")  # OK
user\me\names.txt
[204]:
print("user\me\names.txt")  # not OK
user\me
ames.txt

On peut créer une string sur plusieurs lignes: * en insérant des \n dans le texte pour représenter les sauts de ligne * en créant l’objet string directement avec trois guillemets """

La deuxième option rajoute automatiquement des \n à chaque saut de ligne.

[205]:
print("bla bla\nbla bla\nbla bla")
bla bla
bla bla
bla bla
[206]:
"""bla bla
bla bla
bla bla"""
[206]:
'bla bla\nbla bla\nbla bla'
[207]:
print("""bla bla
bla bla
bla bla""")
bla bla
bla bla
bla bla

Si on doit écrire un long texte dans le code, on peut couper la définition du texte avec un \ ajouté à la fin de chaque ligne.

[208]:
"Ceci est un long texte. Cela peut être un message \
d'erreur. Lorsqu'un si long texte est défini, il est utile de \
le couper avec un backslash à la fin de chaque ligne \
pour éviter d'avoir une instruction qui aille \
à l'encontre de PEP8."
[208]:
"Ceci est un long texte. Cela peut être un message d'erreur. Lorsqu'un si long texte est défini, il est utile de le couper avec un backslash à la fin de chaque ligne pour éviter d'avoir une instruction qui aille à l'encontre de PEP8."

On peut vérifier si deux strings sont égales avec l”opérateur égalité ==. Pour vérifier si une partie d’une string se trouve dans une autre, on utilise le keyword in.

[209]:
"Bob" == "Bill"
[209]:
False
[210]:
"Bob".__eq__("Bill")
[210]:
False
[211]:
"Bob" != "Bill"
[211]:
True
[212]:
"Bob" == 'Bob' == """Bob"""
[212]:
True
[213]:
"Bill" in "Bob and Bill"
[213]:
True
[214]:
"Bob and Bill".__contains__("Bill")
[214]:
True
[215]:
"John" in "Bob and Bill"
[215]:
False
[216]:
"Bill" not in "Bob and Bill"
[216]:
False

Les strings possèdent beaucoup de méthodes pour les analyser et transformer. Lorsqu’une méthode a pour objectif de transformer une string elle retourne un nouvel objet string qui est version transformée de la string initiale. Elles ne le modifient pas directement l’objet initial. En effet, les ``strings`` sont des objets immutables.

[217]:
name = "bob"
print(dir(name))
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
[218]:
name.upper()
[218]:
'BOB'
[219]:
name
[219]:
'bob'
[220]:
name.title()
[220]:
'Bob'
[221]:
name.replace("o", "a")
[221]:
'bab'
[222]:
txt = "   espaces avant et espaces après      "
txt.strip()
[222]:
'espaces avant et espaces après'

strip enlève par défaut les espaces au début et à la fin de la string. Les espaces comprennent les sauts de ligne et les tabulations.

[223]:
import string
string.whitespace
[223]:
' \t\n\r\x0b\x0c'
[224]:
txt.rstrip()
[224]:
'   espaces avant et espaces après'
[225]:
txt.lstrip()
[225]:
'espaces avant et espaces après      '
[226]:
sentence = "quel beau temps"
words = sentence.split()
words
[226]:
['quel', 'beau', 'temps']
[227]:
help(str.split)
Help on method_descriptor:

split(self, /, sep=None, maxsplit=-1)
    Return a list of the words in the string, using sep as the delimiter string.

    sep
      The delimiter according which to split the string.
      None (the default value) means split according to any whitespace,
      and discard empty strings from the result.
    maxsplit
      Maximum number of splits to do.
      -1 (the default value) means no limit.

[228]:
sentence.split("l")
[228]:
['que', ' beau temps']
[229]:
" ".join(words)
[229]:
'quel beau temps'
[230]:
help(str.join)
Help on method_descriptor:

join(self, iterable, /)
    Concatenate any number of strings.

    The string whose method is called is inserted in between each given string.
    The result is returned as a new string.

    Example: '.'.join(['ab', 'pq', 'rs']) -> 'ab.pq.rs'

On peut enchaîner les méthodes tant qu’on fait attention à ce que la méthode qui suit accepte est bien une méthode de l’objet retourné par la méthode qui précède. Dans l’exemple ci-dessous, la méthode strip est bien un méthode de l’objet sentence qui est de type string, elle retourne un objet string qui possède bien une méthode replace, celle-ci retourne un nouvel objet string qui possède bien une méthode split, celle-ci retourne finalement une liste.

[231]:
modified_sentence = sentence.strip("qs").replace("e", "i").split()
modified_sentence
[231]:
['uil', 'biau', 'timp']
[232]:
%reset -f

F-String


Du joli blabla


Les f-strings sont des objets de type string qu’on peut formater. Il faut rajouter un préfixe f devant le premier guillemet, puis ensuite, à l’intérieur, il faut entourer de symboles {} les expressions qu’on souhaite formater afin qu’elles soient directement évaluées par Python, qui construit notre objet string.

[233]:
age = 28
f"J'ai {age} ans"
[233]:
"J'ai 28 ans"
[234]:
type(age)
[234]:
int
[235]:
"J'ai " + age + " ans"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-235-1c648e4e0b43> in <module>
----> 1 "J'ai " + age + " ans"

TypeError: can only concatenate str (not "int") to str
[236]:
age = 28
limite_d_age = 25
if age > limite_d_age:
    print(
        f"Tu as {age} ans, la limite d'âge étant à {limite_d_age}, tu"
        f" as donc {age - limite_d_age} ans de plus que la limite. Tout"
        f" va maintenant être plus cher pour toi!"
    )
Tu as 28 ans, la limite d'âge étant à 25, tu as donc 3 ans de plus que la limite. Tout va maintenant être plus cher pour toi!

Cette manière de formater les strings est assez récente (Python 3.6) et est vraiment pratique et facile à utiliser. Il existe deux autres manières de formater des strings qu’on retrouve encore souvent sur internet.

[237]:
"J'ai %s ans" % (age)
[237]:
"J'ai 28 ans"
[238]:
"J'ai {} ans".format(age)
[238]:
"J'ai 28 ans"
[239]:
%reset -f

Convertir float, int et str


Nombre <—> Texte <—> Nombre


Les fonctions int, float et str tentent de convertir les objets qu’on leur donne. Elles retournent une erreur ``ValueError`` si elles échouent, et l’objet converti si elles réussissent.

[240]:
int(3.7)
[240]:
3
[241]:
float(4)
[241]:
4.0
[242]:
str(3)
[242]:
'3'
[243]:
int("Bob")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-243-b70fd5693779> in <module>
----> 1 int("Bob")

ValueError: invalid literal for int() with base 10: 'Bob'
[244]:
x = "3"
print(type(x))
x = int(x)
print(type(x))
<class 'str'>
<class 'int'>
[245]:
"Bob" + 3
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-245-45c29619c55a> in <module>
----> 1 "Bob" + 3

TypeError: can only concatenate str (not "int") to str
[246]:
"Bob " + str(3)
[246]:
'Bob 3'
[247]:
# print convertit automatiquement les objets en string.
print("Bob", 3)
Bob 3
[248]:
%reset -f

Indexing et Slicing


A piece of cake


Certains objets comme les objets de type string sont des séquences. On peut obtenir n’importe quel item de l’objet grâce à son index numérique (indexing), on peut aussi obtenir directement plusieurs items (slicing). Le premier item d’une séquence a toujours l’index 0 en Python (0-based).

[249]:
txt = "Bill"
[250]:
txt[0]
[250]:
'B'
[251]:
txt.__getitem__(0)
[251]:
'B'
[252]:
txt[1]
[252]:
'i'
[253]:
txt[2]
[253]:
'l'
[254]:
txt[3]
[254]:
'l'
[255]:
txt[4]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-255-e9d47c8188c4> in <module>
----> 1 txt[4]

IndexError: string index out of range
[256]:
txt[-1]
[256]:
'l'
[257]:
txt[-2]
[257]:
'l'
[258]:
txt[-3]
[258]:
'i'
[259]:
txt[-4]
[259]:
'B'
[260]:
txt[-5]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-260-e5b69442cb73> in <module>
----> 1 txt[-5]

IndexError: string index out of range

On peut connaître le nombre d’items d’une séquence avec la fonction ``len``.

[261]:
help(len)
Help on built-in function len in module builtins:

len(obj, /)
    Return the number of items in a container.

Lorsqu’on passe un objet string à la fonction len, on obtient le nombre de caractères de l’objet.

[262]:
len(txt)
[262]:
4
[263]:
txt.__len__()
[263]:
4
[264]:
len("")
[264]:
0
[265]:
len(" ")
[265]:
1

Attention au comportement spécial du backslash \.

[266]:
len("\t")
[266]:
1
[267]:
len("\n")
[267]:
1
[268]:
len("\t")
[268]:
1
[269]:
len(r"\n")
[269]:
2
[270]:
len("Bob and Bill")
[270]:
12
[271]:
len(3.4)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-271-a9f429718b04> in <module>
----> 1 len(3.4)

TypeError: object of type 'float' has no len()

On ne peut pas modifier un item d’une string à partir de son index. En fait, il est simplement impossible de modifier un objet string, il faut forcément en créer un nouveau. Un objet qui ne peut pas être modifié est dit immutable.

[272]:
txt[0] = "A"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-272-dc27665d30a4> in <module>
----> 1 txt[0] = "A"

TypeError: 'str' object does not support item assignment

Pour obtenir plusieurs caractères d’un objet string, on utilise la syntaxe [start:stop:step]start et stop sont des index, et step l’écart entre deux caractères. Par défaut, start est le premier index ([0]), stop est le dernier index, et step est égal à 1. L’élément à l’index ``start`` est inclus dans l’objet string retourné, l’élément à l’index ``stop`` est exclus.

[273]:
numbers = "013456789"
numbers[3::]
[273]:
'456789'
[274]:
numbers[3:]
[274]:
'456789'
[275]:
numbers[-1:]
[275]:
'9'
[276]:
numbers[50::]
[276]:
''
[277]:
numbers[:8]
[277]:
'01345678'
[278]:
numbers[3:8]
[278]:
'45678'
[279]:
numbers[3:4]
[279]:
'4'
[280]:
numbers[3:3]
[280]:
''
[281]:
numbers[3:50]
[281]:
'456789'
[282]:
numbers[::1]
[282]:
'013456789'
[283]:
numbers[::]
[283]:
'013456789'
[284]:
numbers[::2]
[284]:
'03579'
[285]:
numbers[::100]
[285]:
'0'
[286]:
numbers[::-1]
[286]:
'987654310'
[287]:
numbers[::-2]
[287]:
'97530'
[288]:
numbers[3:8:3]
[288]:
'47'
[289]:
numbers[3:8:-1]
[289]:
''
[290]:
numbers[8:3]
[290]:
''
[291]:
numbers[8:3:-1]
[291]:
'98765'
[292]:
numbers[:3:-1]
[292]:
'98765'
[293]:
numbers[-3:-8:-1]
[293]:
'76543'
[294]:
numbers.__getitem__(slice(-3, -8, -1))
[294]:
'76543'
[295]:
%reset -f

Listes et Tuples


Maîtres de l’ordre


Les objets de type list et tuple sont des containers. Ces deux types d’objet sont, comme les objets de type string, des séquences. Ils peuvent contenir n’importe quel type d’objet.

Les lists et les tuples sont très pratiques et utilisés partout dans le langage Python.

[296]:
shopping = ["banana", "beer", "chocolate", "tomato", "salad"]
[297]:
type(shopping)
[297]:
list
[298]:
print(dir(list))
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

L’objet retourné par la fonction dir est une list.

[299]:
type(dir(list))
[299]:
list

On peut accéder à chaque item d’une list par son index numérique.

[300]:
shopping[0]
[300]:
'banana'
[301]:
shopping.__getitem__(0)
[301]:
'banana'

L’index -1 est un raccourci pour indiquer le dernier item. -2 indique l’avant-dernier item, etc.

[302]:
shopping[-1]
[302]:
'salad'
[303]:
shopping[1:4:2]
[303]:
['beer', 'tomato']

La syntaxe ::-1 permet d’inverser la list originale.

[304]:
shopping[::-1]
[304]:
['salad', 'tomato', 'chocolate', 'beer', 'banana']
[305]:
len(shopping)
[305]:
5
[306]:
len([])
[306]:
0

Alors qu’on définit une list en entourant ses items de [], on définit un tuple en entourant ses items de ().

[307]:
shopping = ("banana", "beer", "chocolate", "tomato", "salad")
[308]:
type(shopping)
[308]:
tuple

On accède aux items d’un tuple comme on accède aux items d’une list.

[309]:
shopping[0]
[309]:
'banana'
[310]:
shopping[-1]
[310]:
'salad'
[311]:
shopping[1:4:2]
[311]:
('beer', 'tomato')
[312]:
shopping[::-1]
[312]:
('salad', 'tomato', 'chocolate', 'beer', 'banana')
[313]:
shopping(mess)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-313-b1aa1c29f1d5> in <module>
----> 1 shopping(mess)

NameError: name 'mess' is not defined
[314]:
len(())
[314]:
0

Une list ou dans un tuple peuvent faire référence à n’importe quel type d’objet.

[315]:
import math
big_mess = [2, 2.33, "string", print, math, ["Bob", "Bill"]]
big_mess
[315]:
[2,
 2.33,
 'string',
 <function print>,
 <module 'math' (built-in)>,
 ['Bob', 'Bill']]
[316]:
import math
big_mess = (2, 2.33, "string", print, math, ["Bob", "Bill"])
big_mess
[316]:
(2,
 2.33,
 'string',
 <function print>,
 <module 'math' (built-in)>,
 ['Bob', 'Bill'])

Les ``lists`` et les ``tuples`` ne contiennent aucun objet, ils contiennent en fait des références à d’autres objets.

[317]:
pi = 3.14
[318]:
l = [pi]
[319]:
print(f"""\
{'ID de l:':15}{id(l)}
{'ID de pi:':15}{id(pi)}
{'ID de l[0]:':15}{id(l[0])}
""")
ID de l:       2556174822024
ID de pi:      2556174976056
ID de l[0]:    2556174976056

Comme les strings, les tuples sont des immutables.

[320]:
ages[0] = 55
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-320-3794d71e2f53> in <module>
----> 1 ages[0] = 55

NameError: name 'ages' is not defined

Au contraire, les lists sont des mutables. Il est donc autorisé de changer les éléments qu’elles contiennent, d’en supprimer ou d’en rajouter.

[321]:
shopping = ["banana", "beer", "chocolate", "tomato", "salad"]
print(id(shopping), shopping)
2556174979400 ['banana', 'beer', 'chocolate', 'tomato', 'salad']
[322]:
shopping[0] = "apple"
print(id(shopping), shopping)
2556174979400 ['apple', 'beer', 'chocolate', 'tomato', 'salad']
[323]:
shopping.append("cucumber")
print(id(shopping), shopping)
2556174979400 ['apple', 'beer', 'chocolate', 'tomato', 'salad', 'cucumber']
[324]:
shopping.append("pasta")
print(id(shopping), shopping)
2556174979400 ['apple', 'beer', 'chocolate', 'tomato', 'salad', 'cucumber', 'pasta']
[325]:
del shopping[-1]
print(id(shopping), shopping)
2556174979400 ['apple', 'beer', 'chocolate', 'tomato', 'salad', 'cucumber']
[326]:
del shopping[-2:]
print(id(shopping), shopping)
2556174979400 ['apple', 'beer', 'chocolate', 'tomato']
[327]:
shopping.extend(["egg", "bread", "jam"])
print(id(shopping), shopping)
2556174979400 ['apple', 'beer', 'chocolate', 'tomato', 'egg', 'bread', 'jam']
[328]:
shopping.insert(0, "salt")
print(id(shopping), shopping)
2556174979400 ['salt', 'apple', 'beer', 'chocolate', 'tomato', 'egg', 'bread', 'jam']

Les lists et les tuples supportent les opérateurs in, not in, ==, !=, + et *.

[329]:
"egg" in shopping
[329]:
True
[330]:
"rice" not in shopping
[330]:
True
[331]:
shopping == ["egg", "chocolate"]
[331]:
False
[332]:
shopping != ["egg", "chocolate"]
[332]:
True
[333]:
["egg"] == ("egg")
[333]:
False
[334]:
["egg", "chocolate"][0] == ("egg", "chocolate")[0]
[334]:
True
[335]:
["Bob"] == ["Bill"]
[335]:
False
[336]:
[[["Bob"]]]
[336]:
[[['Bob']]]
[337]:
[[["Bob"]]][0][0][0]
[337]:
'Bob'
[338]:
[[["Bob"]]] == [[["Bill"]]]
[338]:
False
[339]:
["Bob"] + ["Bill"]
[339]:
['Bob', 'Bill']
[340]:
("Bob",) + ("Bill",)
[340]:
('Bob', 'Bill')
[341]:
["Bob", "Bill"] * 3
[341]:
['Bob', 'Bill', 'Bob', 'Bill', 'Bob', 'Bill']
[342]:
("Bob", "Bill") * 3
[342]:
('Bob', 'Bill', 'Bob', 'Bill', 'Bob', 'Bill')
[343]:
[] * 3
[343]:
[]
[344]:
[[]] * 3
[344]:
[[], [], []]

On peut transformer les lists en tuples et vice versa avec les fonctions tuple et list.

[345]:
l = ["Bob", "Bill"]
t = tuple(l)
print(type(t), t)
<class 'tuple'> ('Bob', 'Bill')
[346]:
t = ["Bob", "Bill"]
l = list(t)
print(type(l), l)
<class 'list'> ['Bob', 'Bill']

Les méthodes count et index peuvent être utiles. Elles sont communes aux lists et tuples.

[347]:
[1, 2, 3, 3, 3, 4, 5].count(3)
[347]:
3
[348]:
[1, 2, 3, 3, 3, 4, 5].count(6)
[348]:
0
[349]:
[1, 2, 3, 3, 3, 4, 5].index(4)
[349]:
5
[350]:
[1, 2, 3, 3, 3, 4, 5].index(3)
[350]:
2
[351]:
[1, 2, 3, 3, 3, 4, 5].index(6)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-351-4c30a0c42992> in <module>
----> 1 [1, 2, 3, 3, 3, 4, 5].index(6)

ValueError: 6 is not in list

La fonction sorted permet de trier une list ou un tuple. Elle retourne un nouvel objet qui est une list.

[352]:
sorted([3, 1, 2])
[352]:
[1, 2, 3]

Par défaut les items sont retournés dans l’ordre ascendant, on inverser l’ordre avec le paramètre reverse.

[353]:
sorted((3, 1, 2), reverse=True)
[353]:
[3, 2, 1]

On peut trier des strings mais il faut faire attention à aux différences entre minuscules, capitales et nombres.

[354]:
sorted(["Donald", "Bob", "Bill"])
[354]:
['Bill', 'Bob', 'Donald']
[355]:
"D" > "B"
[355]:
True
[356]:
"D" > "d"
[356]:
False
[357]:
"z" > "A"
[357]:
True
[358]:
"Z" > "0"
[358]:
True

Le paramètre key accepte comme argument une fonction, cette fonction est appliquée à chaque item de l’objet initial et le tri est effectué sur la base des résultats de la fonction. Dans l’exemple ci-dessous, les strings que contient le tuple sont ordonnées en fonction de leur longueur grâce à la built-in fonction len.

[359]:
sorted(("Donald", "Bob", "Bill"), key=len)
[359]:
['Bob', 'Bill', 'Donald']
[360]:
sorted(("Donald", "Bob", "Bill"), key=len, reverse=True)
[360]:
['Donald', 'Bill', 'Bob']
[361]:
sorted?

Attention, lorsqu’on référence une list existante l1 avec un nouveam nom l2, les modifications de l2 modifient l1. Comme déjà évoqué, les noms en Python ne sont que des références à des objets. Dans le cas ci-dessous, la liste [1, 2, 3] est un objet unique auquel on peut accéder par les deux références/noms l1 et l2.

[362]:
l1 = [1, 2, 3]
l2 = l1
l2[0] = 99
print(l1, l2)
[99, 2, 3] [99, 2, 3]
[363]:
l1[1] = 999
print(l1, l2)
[99, 999, 3] [99, 999, 3]
[364]:
id(l1) != id(l2)
[364]:
False

Copier une list est une action qu’on va vouloir faire de temps en temps, il existe deux méthodes. La première effectue une shallow copy de la list source, c’est-à-dire, seuls les références aux objets que contient la list sont copiées.

[365]:
l1 = [1, 2, 3]
l2 = l1[:]  # ou l1.copy()
l2[0] = 999
print(l1, l2)
[1, 2, 3] [999, 2, 3]
[366]:
id(l1) != id(l2)
[366]:
True

Mais si la list fait référence à des containers, les références aux objets qu’ils contiennent ne sont pas copiées.

[367]:
# Première méthode
l1 = [[1], 2, 3]
l2 = l1
l2[0][0] = 999
print(l1, l2)
[[999], 2, 3] [[999], 2, 3]
[368]:
# Deuxième méthode
l1 = [[1], 2, 3]
l2 = l1[:]  # ou l1.copy()
l2[0][0] = 999
print(l1, l2)
[[999], 2, 3] [[999], 2, 3]
[369]:
id(l1) != id(l2)
[369]:
True
[370]:
id(l1[0]) != id(l2[0])
[370]:
False

On peut effectuer une deep copy pour palier au problème précédent. Cette méthode crée autant d’objet que contient la list source.

[371]:
import copy
l1 = [[1], 2, 3]
l2 = copy.deepcopy(l1)
l2[0][0] = 999
print(l1, l2)
[[1], 2, 3] [[999], 2, 3]
[372]:
id(l1[0]) != id(l2[0])
[372]:
True

Même si les objets de type list et tuple sont similaires, en général on ne les utilise pas de la même manière. Les lists vont plutôt contenir des objets du même type (exemple: liste de courses). Les tuples vont contenir une suite d’objets dont on est sûr à l’avance qu’elle n’aura pas besoin d’être modifiée (exemple: prénom et nom).

[373]:
%reset -f

Sets


Vraiment uniques


Les objets de type set sont des containers et sont mutables. Ce ne sont pas des séquences, on ne peut donc pas accéder à leurs item par leur index numérique. Ce type d’objet ne peut contenir que des objets uniques.

[374]:
s = {1, 2, 3}
[375]:
type(s)
[375]:
set
[376]:
print(dir(s))
['__and__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__iand__', '__init__', '__init_subclass__', '__ior__', '__isub__', '__iter__', '__ixor__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__rand__', '__reduce__', '__reduce_ex__', '__repr__', '__ror__', '__rsub__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__xor__', 'add', 'clear', 'copy', 'difference', 'difference_update', 'discard', 'intersection', 'intersection_update', 'isdisjoint', 'issubset', 'issuperset', 'pop', 'remove', 'symmetric_difference', 'symmetric_difference_update', 'union', 'update']

On peut supprimer les objets dupliqués d’une list ou d’un tuple en le transformat en objet set. Attention, l’ordre est items des objets sources n’est pas préservé en les transformant en set.

[377]:
shopping1 = ["banana", "apple", "strawberry", "banana"]
shopping1 = set(shopping1)
shopping1
[377]:
{'apple', 'banana', 'strawberry'}
[378]:
len(shopping1)
[378]:
3
[379]:
shopping1[0]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-379-e297a6d4a0ea> in <module>
----> 1 shopping1[0]

TypeError: 'set' object is not subscriptable
[380]:
shopping1.add("orange")
shopping1
[380]:
{'apple', 'banana', 'orange', 'strawberry'}
[381]:
"orange" in shopping1
[381]:
True

Avec les sets on peut facilement effectuer des opérations de la théorie des ensembles (set theory) comme l’intersection, l’union, la différence, ou la différence symétrique.

[382]:
shopping2 = {"banana", "apple", "mango"}
shopping_intersect = shopping1.intersection(shopping2)
shopping_intersect
[382]:
{'apple', 'banana'}
[383]:
shopping_merged = shopping1.union(shopping2)
shopping_merged
[383]:
{'apple', 'banana', 'mango', 'orange', 'strawberry'}

On peut ensuite convertir un objet set en objet list avec la fonction list.

[384]:
shopping_merged = list(shopping_merged)
shopping_merged
[384]:
['strawberry', 'banana', 'apple', 'orange', 'mango']
[385]:
%reset -f

Dictionnaires


Ils font la paire


Les objets de type dict sont des containers et sont mutables. Tout comme les objets de type set, les dicts ne sont pas des séquences. Ils sont constitués de paires key-value. Les keys doivent être des objets immutables, les values peuvent être n’importe quel type d’objet. Les dictionnaires sont très présents dans le langage Python.

[386]:
phonebook = {
    "Bob": 383,
    "Bill": 509,
    "Donald": 102
}
phonebook
[386]:
{'Bob': 383, 'Bill': 509, 'Donald': 102}
[387]:
type(phonebook)
[387]:
dict
[388]:
print(dir(phonebook))
['__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
[389]:
phonebook["Bob"]
[389]:
383
[390]:
phonebook[383]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-390-9abd3fa563ad> in <module>
----> 1 phonebook[383]

KeyError: 383
[391]:
phonebook["John"]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-391-3f89bbc7ecee> in <module>
----> 1 phonebook["John"]

KeyError: 'John'
[392]:
# Rajouter une key-value entrée
phonebook["John"] = 984
phonebook
[392]:
{'Bob': 383, 'Bill': 509, 'Donald': 102, 'John': 984}
[393]:
phonebook["John"]
[393]:
984
[394]:
# Vérifier si une key est dans le dict
"John" in phonebook
[394]:
True
[395]:
# Cela ne fonctionne que sur les keys, par sur les values
984 in phonebook
[395]:
False
[396]:
# Supprimer une paire key-value
del phonebook["John"]
phonebook["John"]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-396-5eb835ec0b00> in <module>
      1 # Supprimer une paire key-value
      2 del phonebook["John"]
----> 3 phonebook["John"]

KeyError: 'John'
[397]:
# Les dicts ne sont pas des séquences.
phonebook[0]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-397-6cd85588bf81> in <module>
      1 # Les dicts ne sont pas des séquences.
----> 2 phonebook[0]

KeyError: 0
[398]:
# La longueur d'un dict est le nombre de paires key-value qu'il contient.
len(phonebook)
[398]:
3
[399]:
# Seuls des immutables peuvent être des keys
mess = {
    "str": "Les strings sont immutables",
    12: "Les ints sont immutables",
    3.53: "Les floats sont immutables",
    (12, 24): "Les tuples sont immutables"
}
[400]:
# Les lists sont mutables, elles ne peuvent pas être des keys
mess[["list as a key"]] = 1
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-400-1f3405e3d82b> in <module>
      1 # Les lists sont mutables, elles ne peuvent pas être des keys
----> 2 mess[["list as a key"]] = 1

TypeError: unhashable type: 'list'
[401]:
# Ici la key est un tuple, on sait que les tuples sont immutables.
# Mais ce tuple-là elle contient une liste, qui est un objet mutable
# La key n'est donc pas considérée comme une key valide.
mess[(["tuple as a key"], ["containing a list"])] = 1
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-401-28cb6015e709> in <module>
      2 # Mais ce tuple-là elle contient une liste, qui est un objet mutable
      3 # La key n'est donc pas considérée comme une key valide.
----> 4 mess[(["tuple as a key"], ["containing a list"])] = 1

TypeError: unhashable type: 'list'
[402]:
# Une autre manière de créer un dict.
{"name": ["Bob", "Bill"], "age": [24, 23]} == dict(name=["Bob", "Bill"], age=[24, 23])
[402]:
True
[403]:
# Les values d'un dict peuvent être de n'importe quel type.
import math
d = {
    3.13: math,
    "print": print,
    "l": [1, 2, 3],
    123: "123",
    "nested_dicts": {"another_one": {"and_a_last_one": "here I am!"}},
    "sets": {1, 3, 5},
    True: False
}
d
[403]:
{3.13: <module 'math' (built-in)>,
 'print': <function print>,
 'l': [1, 2, 3],
 123: '123',
 'nested_dicts': {'another_one': {'and_a_last_one': 'here I am!'}},
 'sets': {1, 3, 5},
 True: False}
[404]:
d["nested_dicts"]["another_one"]["and_a_last_one"]
[404]:
'here I am!'

La méthode get est pratique, elle retourne la value de la key passée comme argument. Si la key n’est pas trouvée, elle retourne None par défaut ou la valeur du deuxième argument passé.

[405]:
d.get(123, "found")
[405]:
'123'
[406]:
d.get(456, "not found")
[406]:
'not found'
[407]:
d.get?

On peut obtenir une list des keys ou des values en convertissant les objets retournés par les méthodes keys et values avec la fonction list.

[408]:
list(d.keys())
[408]:
[3.13, 'print', 'l', 123, 'nested_dicts', 'sets', True]
[409]:
list(d.values())
[409]:
[<module 'math' (built-in)>,
 <function print>,
 [1, 2, 3],
 '123',
 {'another_one': {'and_a_last_one': 'here I am!'}},
 {1, 3, 5},
 False]

La méthode pop enlève la key spécifiée et retourne la valeur correspondante.

[410]:
d.pop(3.13)

[410]:
<module 'math' (built-in)>
[411]:
d
[411]:
{'print': <function print>,
 'l': [1, 2, 3],
 123: '123',
 'nested_dicts': {'another_one': {'and_a_last_one': 'here I am!'}},
 'sets': {1, 3, 5},
 True: False}
[412]:
%reset -f

Fonctions


Don’t Repeat Yourself


On a déjà vu quelques unes des fonctions intégrées directement dans Python (built-in functions). On peut en fait créer nos propres fonctions. Cela est bien utile lorsqu’on souhaite réutiliser un bout de code, et/ou lorsqu’on souhaite faciliter la lecture/compréhension du code en le scindant en blocs logiques. On crée un objet function avec le keyword ``def``.

[413]:
def say_hi():
    print("hi")
[414]:
type(say_hi)
[414]:
function
[415]:
print(dir(say_hi))
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

Le code sous la définition de la fonction say_hi est exécuté lorsqu’on l’appelle.

[416]:
say_hi()
hi

Une fonction peut avoir des paramètres. On les précise sur la ligne de définition de la fonction. Ils peuvent ensuite être utilisés dans la fonction. La fonction add a deux paramètres x et y.

[417]:
def add(x, y):
    print(x + y)

La fonction add est appelée avec les arguments 3 et 4.

[418]:
add(3, 4)
7
[419]:
result = add(3, 4)
7
[420]:
result
[421]:
result is None
[421]:
True

Une fonction ne retourne qu’un seul et unique objet, qui est ``None`` par défaut. On définit l’objet retourné en le précédant du keyword ``return``.

[422]:
def add(x, y):
    return x + y
[423]:
result = add(3, 4)
[424]:
result
[424]:
7

L’objet string entouré de """ et positionné au début du bloc de la fonction est la docstring de la fonction.

[425]:
def add(x, y):
    """Additioner deux objets."""
    return x + y
[426]:
add.__doc__
[426]:
'Additioner deux objets.'
[427]:
help(add)
Help on function add in module __main__:

add(x, y)
    Additioner deux objets.

Comme une fonction est un objet, on peut la manipuler comme les autres objets.

[428]:
add2 = add
[429]:
add2(2, 3)
[429]:
5
[430]:
# del add enlève le nom add du namespace
del add
add2(0, 2)
[430]:
2

Certains paramètres peuvent avoir des valeurs par défaut. Lorsqu’on appelle la fonction, on n’est donc pas obligé de préciser la valeur de ce paramètre (ce qui équivaut à dire qu’on n’est pas obligé de fournir un argument pour ce paramètre). Dans ce cas-là, la valeur par défaut est utilisée lors de l’exécution du code. Lorsqu’on souhaite utiliser une autre valeur que la valeur par défaut, on peut soit fournir l’argument si on respecte l’ordre des paramètres, on peut aussi fournir l’argument en nommant le paramètre (``param=arg``).

[431]:
print?
[432]:
def function_with_defaults(a, b, c=0):
    return a + b + c
function_with_defaults(1, 4)
[432]:
5
[433]:
function_with_defaults(1, 4, c=50)
[433]:
55
[434]:
function_with_defaults(1, 4, 5)
[434]:
10
[435]:
function_with_defaults(c=50, 1, 4)
  File "<ipython-input-435-e132c12cc603>", line 1
    function_with_defaults(c=50, 1, 4)
                                ^
SyntaxError: positional argument follows keyword argument

[436]:
function_with_defaults(c=50, b=4, a=1)
[436]:
55

Une fonction peut appeler une autre fonction.

[437]:
def first():
    print("first function called")
    second()

def second():
    print("second function called")
[438]:
first()
first function called
second function called

Lorsqu’on exécute le bloc de définition d’une fonction (la ligne def et le bloc indenté qui suit), Python se prépare à exécuter la fonction mais ne l’exécute pas encore, elle ne le sera que lorsqu’elle sera appelée. A part les erreurs de syntaxe (SyntaxError) que Python détecte lors de cette étape de préparation, toutes les autres erreurs sont détectées seulement lorsque la fonction est appelée.

[439]:
unknown
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-439-2d7bb5572221> in <module>
----> 1 unknown

NameError: name 'unknown' is not defined
[440]:
# unknown n'est pas une variable définie, malgré ça, on peut exécuter ce code
def dont_even_dare():
    return unknown
[441]:
# On obtient une erreur lorsqu'on appelle la fonction dont_even_dare
dont_even_dare()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-441-a64b5e067227> in <module>
      1 # On obtient une erreur lorsqu'on appelle la fonction dont_even_dare
----> 2 dont_even_dare()

<ipython-input-440-c0383c067b3c> in dont_even_dare()
      1 # unknown n'est pas une variable définie, malgré ça, on peut exécuter ce code
      2 def dont_even_dare():
----> 3     return unknown

NameError: name 'unknown' is not defined

L’exécution d’une fonction se passe dans son scope local. Les objets référencés par la fonction (firstname = "Bob") sont déférencés lorsque l’exécution prend fin. On ne peut plus y avoir accès après ça.

[442]:
def function_scope():
    firstname = "Bob"
function_scope()
firstname
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-442-88897d2c37bf> in <module>
      2     firstname = "Bob"
      3 function_scope()
----> 4 firstname

NameError: name 'firstname' is not defined

Même la référence de l’objet retourné par la fonction est supprimée à la fin de l’exécution.

[443]:
def function_scope():
    firstname = "Bob"
    return firstname
function_scope()
firstname
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-443-adec7aecec8c> in <module>
      3     return firstname
      4 function_scope()
----> 5 firstname

NameError: name 'firstname' is not defined

Pour préserver l’objet retourné on lui associe un nom.

[444]:
firstname = function_scope()
firstname
[444]:
'Bob'

x ci-dessous n’est ni un paramètre de la fonction look_outside ni un objet créé dans la fonction. Lorsqu’on exécute look_outside, Python regarde d’aborde dans le scope local de la fonction s’il existe une référence x. Comme il ne la trouve pas, Il cherche alors dans le scope global de la fonction.

Dans le premier cas ci-dessous, aucune référence x n’est créée en dehors du scope de la fonction. Une erreur est donc déclenchée.

[445]:
def look_outside(b):
    return x + b

look_outside(20)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-445-0e88279aaa1d> in <module>
      2     return x + b
      3
----> 4 look_outside(20)

<ipython-input-445-0e88279aaa1d> in look_outside(b)
      1 def look_outside(b):
----> 2     return x + b
      3
      4 look_outside(20)

NameError: name 'x' is not defined

Ici, comme la référence x est créée (ajoutée au namespace) avant l’exécution de look_outside, celle-ci peut la trouver et s’exécute dont jusqu’à son terme.

[446]:
def look_outside(b):
    return x + b

x = 10
look_outside(20)
[446]:
30
[447]:
def look_inside_first(b):
    x = 0
    return x + b

x = 10
look_inside_first(20)
[447]:
20

Dans le cas ci-dessous, outer_function est appelée avec b=20. Lors de son exécution, la fonction inner_function est d’abord définie. Puis outer_function tente de retourner x + inner_function() + b. Elle trouve une référence à x dans le scope global. Elle exécute ensuite inner_function. Cette dernière doit retourne la référence y, sauf que cette référence n’est définie nulle part. Une erreur est donc déclenchée.

[448]:
def outer_function(b):
    def inner_function():
        return y
    return x + inner_function() + b

x = 10
outer_function(20)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-448-8a9c831108bd> in <module>
      5
      6 x = 10
----> 7 outer_function(20)

<ipython-input-448-8a9c831108bd> in outer_function(b)
      2     def inner_function():
      3         return y
----> 4     return x + inner_function() + b
      5
      6 x = 10

<ipython-input-448-8a9c831108bd> in inner_function()
      1 def outer_function(b):
      2     def inner_function():
----> 3         return y
      4     return x + inner_function() + b
      5

NameError: name 'y' is not defined

Le code va s’exécuter correctement tant que la référence à y est créée avant ou pendant l’exécution de inner_function.

[449]:
def outer_function(b):
    y = 100
    def inner_function():
        return y
    return x + inner_function() + b

x = 10
outer_function(20)
[449]:
130
[450]:
def outer_function(b):
    def inner_function():
        y = 100
        return y
    return x + inner_function() + b

x = 10
outer_function(20)
[450]:
130
[451]:
def outer_function(b):
    def inner_function():
        return y
    y = 100
    return x + inner_function() + b

x = 10
outer_function(20)
[451]:
130
[452]:
def outer_function(b):
    def inner_function():
        return y
    return x + inner_function() + b

x = 10
y = 100
outer_function(20)
[452]:
130

Attention aux objets mutables passés comme argument à une fonction. Lorsqu’on passe un argument à une fonction, celle-ci utilise directement l’objet référencé.

[453]:
def is_same_objet(i, f, s, l, t, se, d):
    print(id(i) == id(io))
    print(id(f) == id(fo))
    print(id(s) == id(so))
    print(id(l) == id(lo))
    print(id(t) == id(to))
    print(id(se) == id(seo))
    print(id(d) == id(do))

io, fo, so, lo, to, seo, do = 1, 2.2, "", list(), tuple(), set(), dict()
is_same_objet(io, fo, so, lo, to, seo, do)
True
True
True
True
True
True
True

Si on passe un objet mutable comme argument d’une fonction, cette fonction va utiliser directement cet objet. S’il est modifié au cours de l’exécution de la fonction, c’est le bien mutable source qui est modifié, même s’il n’a pas la même référence dans la fonction.

[454]:
def achtung_mutable(mutable):
    mutable[0] = "John"
    return mutable
[455]:
new_names = achtung_mutable(names)
new_names
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-455-82436503c9a1> in <module>
----> 1 new_names = achtung_mutable(names)
      2 new_names

NameError: name 'names' is not defined
[456]:
# names a aussi été modifié!
names
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-456-2e452de3783e> in <module>
      1 # names a aussi été modifié!
----> 2 names

NameError: name 'names' is not defined

Si ce comportement n’est pas souhaité, on peut soit appeler la fonction avec une copie du mutable, soit créer une copie directement dans le bloc de la fonction.

[457]:
names = ["Bob", "Bill"]
new_names = achtung_mutable(names[:])
print(new_names, names)
['John', 'Bill'] ['Bob', 'Bill']
[458]:
def achtung_mutable(mutable):
    mutable = mutable[:]
    mutable[0] = "John"
    return mutable

names = ["Bob", "Bill"]
new_names = achtung_mutable(names[:])
print(new_names, names)
['John', 'Bill'] ['Bob', 'Bill']
[459]:
%reset -f

Unpacking, Packing


Déballer son sac


On peut facilement extraire un à un les éléments d’un objet iterable (séquences et containers) pour assigner de nouvelles variables avec la méthode dite de l”unpacking. Cela se fait le plus souvent à partir d’un tuple.

[460]:
t = ("Bob", 27, ["Sarah", "Jim"])
[461]:
type(t)
[461]:
tuple

t comprend trois éléments, ces trois éléments sont assignés dans l’ordre aux variables name, age et siblings.

[462]:
name, age, siblings = t
print(name, age, siblings)
Bob 27 ['Sarah', 'Jim']

Mais on peut aussi le faire avec d’autres types d’objet (avec plus ou moins de succès).

[463]:
l = ["Bob", 27, ["Sarah", "Jim"]]
name, age, siblings = l
print(name, age, siblings)
Bob 27 ['Sarah', 'Jim']
[464]:
s = "abc"
a, b, c = s
print(a, b, c)
a b c
[465]:
# Attention! Les keys des dictionnaires ne sont ordonnées
# que depuis la version 3.6.
d = {"name": "Bob", "surname": "Smith"}
n, s = d
print(n, s)
name surname
[466]:
# Mauvaise idée!
s = {"a", "b", "c"}
a, b, c = s
print(a, b, c)
c a b
[467]:
x = 3.4
pint, pdec = 3.4
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-467-5c0c97d6e39b> in <module>
      1 x = 3.4
----> 2 pint, pdec = 3.4

TypeError: cannot unpack non-iterable float object

Il est préférable que le nombre de variable à assigner soit égal au nombre d’éléments qui constituent l”iterable.

[468]:
a, b = ("a")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-468-3d0636e9e44a> in <module>
----> 1 a, b = ("a")

ValueError: not enough values to unpack (expected 2, got 1)
[469]:
a, b = ("a", "b", "c")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-469-059e52dab1eb> in <module>
----> 1 a, b = ("a", "b", "c")

ValueError: too many values to unpack (expected 2)

Si ce n’est pas le cas, on peut rajouter un symbole * devant le nom d’une des variables pour déclencher un comportement spécial.

[470]:
l = ("First value of interest", "big", "mess", "after", "that")
myvalue, *mess = l
print(myvalue, mess)
First value of interest ['big', 'mess', 'after', 'that']

On peut utiliser l”unpacking et le symbole * pour passer tous les éléments extraits d’un iterable à une fonction.

[471]:
names = ["Rachel", "Sarah", "Bob", "Bill"]
print(names)
['Rachel', 'Sarah', 'Bob', 'Bill']
[472]:
print(*names)
Rachel Sarah Bob Bill
[473]:
*names
  File "<ipython-input-473-189f4c92eaa6>", line 4
SyntaxError: can't use starred expression here

Packing est l’action par laquelle plusieurs objets sont rassemblés dans un tuple.

[474]:
"Bob", 27, ["Sarah", "Jim"]
[474]:
('Bob', 27, ['Sarah', 'Jim'])
[475]:
packed = "Bob", 27, ["Sarah", "Jim"]
packed
[475]:
('Bob', 27, ['Sarah', 'Jim'])
[476]:
type(packed)
[476]:
tuple

Lorsqu’on essaie de retourner plusieurs objets dans une fonction, ceux-ci sont en fait toujours rassemblés dans un ``tuple``. Packing est à l’oeuvre.

[477]:
def person_info():
    return "Anna", 26  # --> return (name, age)

output = person_info()
type(output)
[477]:
tuple

On peut utiliser l”unpacking pour assigner à des variables les objets retournés par la fonction person_info.

[478]:
name, age = person_info()
print(name, age)
Anna 26
[479]:
%reset -f

Boucle for et fonctions associées


May the for be with you


Les boucles ``for`` sont très simples et utiles en Python. Elles permettent de répéter un bloc d’instructions un certain nombre de fois. L’objet qui suit le keyword in doit être un iterable.

[480]:
for i in range(3):
    print(i)
0
1
2
[481]:
range?

range est une fonction qui accepte 3 arguments start, stop et step. Par défaut start est égal à 0 et step à 1. Cette fonction ne retourne pas une liste mais un obtjet de type range. Il s’agit d’un objet un peu spécial. Lorsqu’on exécute range(1_000_001), considérant le code de la cellule précédente, on pourrait penser que cette instruction génère des nombres de 0 à 1000000, mais non. Cet objet est une sorte de distributeur, il ne fait rien tant qu’on ne lui demande rien. Lorsqu’on lui demande quelque chose, comme dans une boucle for, il s’active et commence à distribuer. Dans le cas de range(1_000_001), l’objet distribue des ints en commençant à 0, un par un, jusqu’à 1000000.

[482]:
for i in range(3):
    print(id(i))
140734432776992
140734432777024
140734432777056
[483]:
type(range(3))
[483]:
range

Pour créer une liste croissante d’entiers, on peut convertir un objet range en liste avec la fonction list. Alors que range(100_000_001) occupe très peu de mémoire, liste(range(100_000_001)) va occuper plus de 900Mb (ne pas exécuter ça!).

[484]:
list(range(3))
[484]:
[0, 1, 2]

Pour itérer au travers d’une list, on pourrait écrire le code suivant: * On génère des ints avec la fonction range, le nombre d’ìntsà générer dépend de la longueur de lalistqu'on calcule avec la fonctionlen* On accède à chaque item de lalist` par son indice

[485]:
names = ["Sarah", "Rachel", "Bob", "Bill"]
for i in range(len(names)):
    print(names[i])
Sarah
Rachel
Bob
Bill

Même si la méthode ci-dessus fonctionne, elle n’est en fait pas pythonic, elle s’inspire de la manière dont on écrit une boucle for dans d’autres langages. Python propose une manière plus claire d’itérer au travers d’un objet. La boucle for de Python est en fait similaire à une boucle for each, elle extrait chaque élément de l”iterable sur laquelle elle agit.

[486]:
for name in names:
    print(name)
Sarah
Rachel
Bob
Bill

On peut pratiquer l”unpacking dans la ligne de la boucle for.

[487]:
fullnames = [("Rachel", "Miller"), ("Bob", "Smith"), ("Anna", "Johnson"), ("Bill", "Davis")]
for first_name, last_name in fullnames:
    print(first_name, last_name)
Rachel Miller
Bob Smith
Anna Johnson
Bill Davis

On peut utiliser la boucle for pour itérer au travers des objets de type string, list, tuple, dict, set (et plus encore).

[488]:
txt = "abcdefghi"
for letter in txt:
    print(letter)
a
b
c
d
e
f
g
h
i
[489]:
names = ("Bob", "Bill", "Sarah", "Anna")
for name in names:
    print(name)
Bob
Bill
Sarah
Anna
[490]:
phonebook = {
    "Bob": 383,
    "Bill": 509,
    "Donald": 102
}
for k in phonebook:
    print(k)
Bob
Bill
Donald
[491]:
for k, v in phonebook.items():
    print(k, v)
Bob 383
Bill 509
Donald 102
[492]:
for v in phonebook.values():
    print(v)
383
509
102

On peut inverser l’ordre des itérations en appelant la fonction reversed sur l”iterable.

[493]:
for name in reversed(names):
    print(name)
Anna
Sarah
Bill
Bob

Un peu comme la fonction range, la fonction reversed ne retourne pas une list mais un objet qualifié d”iterator. Il s’agit aussi d’un distributeur, il distribue dans ce cas-là les éléments de l”iterable en commençant par le dernier. Comme on peut itérer sur un iterator, ce type d’objet est aussi un iterable.

[494]:
reversed(names)
[494]:
<reversed at 0x25327e62048>
[495]:
type(reversed(names))
[495]:
reversed
[496]:
reversed(names)[0]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-496-0f87fe6cd96c> in <module>
----> 1 reversed(names)[0]

TypeError: 'reversed' object is not subscriptable

Les iterators sont des objets qui se tarissent. Après leur avoir demandé de distribuer leur « contenu » une fois, ils ne peuvent plus le redistribuer.

[497]:
reversed_names = reversed(names)
list(reversed_names)
[497]:
['Anna', 'Sarah', 'Bill', 'Bob']
[498]:
list(reversed_names)
[498]:
[]

reserved_names est maintenant un iterator vide, on ne peut plus le réutiliser. (A noter que l’objet retourné par range ressemble à un iterator mais n’en est en fait pas un, on peut donc le réutiliser).

[499]:
for name in sorted(names):
    print(name)
Anna
Bill
Bob
Sarah
[500]:
for name in sorted(names, key=len):
    print(name)
Bob
Bill
Anna
Sarah

sorted ne retourne pas un iterator mais directement une list.

[501]:
sorted(names)
[501]:
['Anna', 'Bill', 'Bob', 'Sarah']
[502]:
type(sorted(names))
[502]:
list

La fonction ``enumerate`` fournit sous la forme d’un ``tuple`` un compteur et les les éléments de l’iterable avec laquelle elle est appelée.

[503]:
for i, name in enumerate(names):
    print(f"Iteration {i}: {name}")
Iteration 0: Bob
Iteration 1: Bill
Iteration 2: Sarah
Iteration 3: Anna
[504]:
for i, name in enumerate(reversed(names)):
    print(f"Iteration {i}: {name}")
Iteration 0: Anna
Iteration 1: Sarah
Iteration 2: Bill
Iteration 3: Bob

L’objet que la fonction enumerate retourne est un iterator.

[505]:
enumerate(names)
[505]:
<enumerate at 0x25327e98ca8>
[506]:
enumerated_names = enumerate(names)
type(enumerated_names)
[506]:
enumerate
[507]:
list(enumerated_names)
[507]:
[(0, 'Bob'), (1, 'Bill'), (2, 'Sarah'), (3, 'Anna')]
[508]:
list(enumerated_names)
[508]:
[]

La function zip permet d’itérer sur plusieurs iterables en extrayant leur élément en même temps. L’objet qu’elle retourne est un iterator.

[509]:
ages = [25, 28, 23, 21]
first_letters = ["B", "R", "B", "A"]
[510]:
for name, first_letter, age in zip(names, first_letters, ages):
    print(name, first_letter, age, sep="   ---    ")
Bob   ---    B   ---    25
Bill   ---    R   ---    28
Sarah   ---    B   ---    23
Anna   ---    A   ---    21
[511]:
for i, (name, first_letter, age) in enumerate(zip(names, first_letters, ages)):
    print(i, name, first_letter, age, sep="   ---    ")
0   ---    Bob   ---    B   ---    25
1   ---    Bill   ---    R   ---    28
2   ---    Sarah   ---    B   ---    23
3   ---    Anna   ---    A   ---    21
[512]:
zipped = zip(names, first_letters, ages)
type(zipped)
[512]:
zip
[513]:
list(zipped)
[513]:
[('Bob', 'B', 25), ('Bill', 'R', 28), ('Sarah', 'B', 23), ('Anna', 'A', 21)]
[514]:
list(zipped)
[514]:
[]

Mais que se passe-t-il exactement lorsqu’on exécute une boucle for? A chaque itération, un élément de l”iterable est référencé par le nom défini juste après le keyword for. On peut voir que les objets extraits d’une list sont exactement les objets auxquels elle fait référence.

[515]:
for i, name in enumerate(names):
    print(id(names[i]), id(name), sep="   ---   ")
2556174778640   ---   2556174778640
2556174782000   ---   2556174782000
2556174782056   ---   2556174782056
2556174781552   ---   2556174781552

On a souvent besoin de modifier un à un les éléments d’une ``list`` (exemple: mettre des noms en capitales). On peut commettre l’erreur de l’implémenter de la manière suivante.

[516]:
for name in names:
    name = name.upper()
names
[516]:
('Bob', 'Bill', 'Sarah', 'Anna')

Cela ne fonctionne pas car l’instruction name = name.upper() crée un nouvel objet dont la valeur est name.upper() et qui est associé au nom name. La référence à l’élément de names par le nom name est donc perdue (écrasée, remplacée).

[517]:
for i, name in enumerate(names):
    print(f"name(id={id(name)}, valeur=names[{i}])")
    name = name.upper()
    print(f"name(id={id(name)}, valeur=names[{i}].upper()]", end="\n\n")
name(id=2556174778640, valeur=names[0])
name(id=2556175279808, valeur=names[0].upper()]

name(id=2556174782000, valeur=names[1])
name(id=2556175279808, valeur=names[1].upper()]

name(id=2556174782056, valeur=names[2])
name(id=2556175279808, valeur=names[2].upper()]

name(id=2556174781552, valeur=names[3])
name(id=2556175279808, valeur=names[3].upper()]

Une autre option serait d’accéder à chaque élément de la list.

[518]:
for i in range(len(names)):
    names[i] = names[i].upper()
names
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-518-2bb71f59e7d7> in <module>
      1 for i in range(len(names)):
----> 2     names[i] = names[i].upper()
      3 names

TypeError: 'tuple' object does not support item assignment

Mais la méthode recommandée est la suivante: * On crée une list vide avec l = [] * A chaque itération on vient rajouter un élément à cette liste vide avec la méthode append (l.append(new_item))

[519]:
names = ["Bob", "Bill", "Sarah", "Rachel"]
upper_names = []
for name in names:
    upper_names.append(name.upper())
upper_names
[519]:
['BOB', 'BILL', 'SARAH', 'RACHEL']

On peut inclure des conditions if dans le bloc d’une boucle for pour préciser les actions à effectuer.

[520]:
for name in names:
    if "s" in name.lower():
        if "a" in name.lower():
            if "r" in name.lower():
                print(f"I think you're Sarah, correct? Hmm I'm {name}!")
I think you're Sarah, correct? Hmm I'm Sarah!

Cela génère des fois du code qui est très indenté et pas facile à lire, on peut utiliser les *keywords* ``continue`` et ``break`` pour contrôler l’exécution de la boucle. continue entraîne le démarrage direct de la prochaine itération, ainsi, tout le code qui est situé sous continue est passé si continue est exécuté. break arrête totalement l’exécution de la boucle.

[521]:
for name in names:
    if "s" not in name.lower():
        continue
    if "a" not in name.lower():
        continue
    if "r" not in name.lower():
        continue
    # Exécuté si les trois conditions ci-dessus sont fausses
    print(f"I think you're Sarah, correct? Hmm I'm {name}!")
    break

# On aurait aussi pu écrire le code de la manière suivante:
# for name in names:
#     if all(c in name.lower() for c in "sar"):
#         print(f"I think you're Sarah, correct? Hmm I'm {name}!")
#         break
I think you're Sarah, correct? Hmm I'm Sarah!
[522]:
for name in names:
    if name == "Sarah":
        continue
    print(name)  # Exécuté si name != "Sarah"
Bob
Bill
Rachel
[523]:
for name in names:
    if name == "Sarah":
        break  # Stoppe la boucle lorsque name == "Sarah"
    print(name)
Bob
Bill

Contrairement aux références créés dans le scope d’une fonction, les références créés dans un bloc for ne sont pas supprimées à la fin de l’exécution du bloc. La référence name est donc toujours accessible et sa valeur est celle de la dernière itération. Ici on voit que sa valeur est 'Sarah', ce qui a effectivement stoppé l’exécution de la boucle.

[524]:
name
[524]:
'Sarah'

Boucle while


On ne l’arrête plus


La boucle for en Python est tellement pratique que la boucle while est au final assez peu utilisée. Les instructions d’une boucle while sont exécutées tant que la condition qui suit le keyword while est vraie. On peut aussi utiliser les keywords continue et break pour contrôler l’exécution de la boucle.

[525]:
i = 0
while i < 5:
    print(i)
    i += 1
0
1
2
3
4

Il peut être plus naturel d’écrire une boucle while au lieu d’une boucle for lorsque l’on recherche quelque chose dans un iterable.

[526]:
letters = "abcdefghijklmnopqrstuvwxyz"
index_o = 0
while letters[index_o] != "o":
    index_o += 1
print(f"La lettre `o` est la lettre n°{index_o + 1} de l'alphabet")
La lettre `o` est la lettre n°15 de l'alphabet
[527]:
for i, l in enumerate(letters):
    if l == "u":
        index_u = i
        break
print(f"La lettre `u` est la lettre n°{index_u + 1} de l'alphabet")
La lettre `u` est la lettre n°21 de l'alphabet

Un des dangers de la boucle ``while`` est qu’on peut créer une boucle qui ne s’arrête jamais. Le code ci-dessous va s’exécuter à l’infini, la condition i < 5 étant toujours vraie (on a oublié d’incrémenter i). Il faut alors forcer l’arrêt de l’interpréteur Python.

[528]:
# i = 0
#  while i < 5:
#    print(i)

Lire et écrire dans un fichier


Cours Primaire


On peut facilement lire le contenu d’un fichier texte et écrire du texte dans un fichier. Pour cela, on utilise: * la fonction open qui permet d’ouvrir un fichier et de le lire ou d’y écrire quelque chose * le keyword with qui signale l’utilisation d’un objet qualifié de context manager (ici, la fonction open). L’emploi de ce genre d’objet avec un bloc with permet de s’assurer de la fermeture de la ressource ouverte (ici, un fichier), même si une erreur se produit dans l’exécution du bloc de code

[529]:
%%writefile data.txt
name,age,gender
Sarah,27,F
Bob,28,M
Rachel,24,F
Bill,22,M
Writing data.txt

La fonction open ouvre le fichier en mode lecture ("r" pour read) et retourne un objet référencé par f (on peut choisir n’importe quel nom) et lié au fichier en cours de lecture.

[530]:
with open("data.txt", "r") as f:
    print(type(f))
    print(dir(f))
<class '_io.TextIOWrapper'>
['_CHUNK_SIZE', '__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_checkClosed', '_checkReadable', '_checkSeekable', '_checkWritable', '_finalizing', 'buffer', 'close', 'closed', 'detach', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'line_buffering', 'mode', 'name', 'newlines', 'read', 'readable', 'readline', 'readlines', 'reconfigure', 'seek', 'seekable', 'tell', 'truncate', 'writable', 'write', 'write_through', 'writelines']

On peut vérifier que le fichier est bien fermé par le context manager.

[531]:
with open("data.txt", "r") as f:
    print(f.closed)

print(f.closed)
False
True

Pour l’instant l’exécution des instructions du bloc with s’est déroulée sans erreur. On peut montrer que le context manager ferme bien le fichier même si une erreur survient dans le bloc.

[532]:
f.closed
[532]:
True
[533]:
with open("data.txt", "r") as f:
    print("Fichier ouvert?", not f.closed)
    print(f.wrong_attribute_to_trigger_an_error)
    print("Rien ne va plus")
Fichier ouvert? True
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-533-69c400717321> in <module>
      1 with open("data.txt", "r") as f:
      2     print("Fichier ouvert?", not f.closed)
----> 3     print(f.wrong_attribute_to_trigger_an_error)
      4     print("Rien ne va plus")

AttributeError: '_io.TextIOWrapper' object has no attribute 'wrong_attribute_to_trigger_an_error'
[534]:
print("Fichier ouvert?", not f.closed)
Fichier ouvert? False

On peut lire le contenu complet du fichier grâce aux méthodes read et readlines.

[535]:
with open("data.txt", "r") as f:
    whole_content = f.read()
whole_content
[535]:
'name,age,gender\nSarah,27,F\nBob,28,M\nRachel,24,F\nBill,22,M\n'
[536]:
print(whole_content)
name,age,gender
Sarah,27,F
Bob,28,M
Rachel,24,F
Bill,22,M

[537]:
with open("data.txt", "r") as f:
    lines = f.readlines()
lines
[537]:
['name,age,gender\n',
 'Sarah,27,F\n',
 'Bob,28,M\n',
 'Rachel,24,F\n',
 'Bill,22,M\n']
[538]:
for line in lines:
    print(line)
name,age,gender

Sarah,27,F

Bob,28,M

Rachel,24,F

Bill,22,M

Chaque ligne du fichier se termine par un caractère retourne à la ligne. On peut utiliser la méthode rstrip des objets de type string pour retirer ce caractère spécial.

[539]:
for line in lines:
    print(line.strip())
name,age,gender
Sarah,27,F
Bob,28,M
Rachel,24,F
Bill,22,M

Mais on peut aussi le lire ligne par ligne en itérant sur l’objet retourné par la fonction ``open``. Cet objet est en fait un iterator.

[540]:
with open("data.txt", "r") as f:
    for line in f:
        print(line.strip())
name,age,gender
Sarah,27,F
Bob,28,M
Rachel,24,F
Bill,22,M

Comme f est un iterator, on ne peut itérer dessus qu’un seule fois.

[541]:
with open("data.txt", "r") as f:
    for line in f:
        print(line.strip())
    for line in f:
        print(line.strip())
name,age,gender
Sarah,27,F
Bob,28,M
Rachel,24,F
Bill,22,M

L’avantage de lire un fichier ligne par ligne est double: * On ne charge pas l’ensemble des données du fichier dans la mémoire * On peut arrêter sa lecture avant la fin si on cherche quelque chose de précis

[542]:
with open("data.txt", "r") as f:
    for i, line in enumerate(f):
        if "Bob" in line:
            print(f"I've found Bob @line {i + 1} !!!")
            break  # Stoppe la lecture du fichier
I've found Bob @line 3 !!!
[543]:
with open("data.txt", "r") as f:
    for i, line in enumerate(f):
        line = line.strip()
        if line.startswith("Bob"):
            age = line.split(",")[1]
            break
print(f"Bob is {age} yo.")
Bob is 28 yo.

Pour écrire dans un nouveau fichier, la démarche est tout à fait similare. Au lieu de passer l’argument r à la fonction open, on lui passe l’argument w (pour write). La méthode write n’accepte comme argument que des objets de type string. Au besoin, on convertit les objets (float) par exemple en string en les passant comme argument à la fonction str.

[544]:
names = ["Sarah", "Rachel", "Bob", "Bill"]
with open("new_data.txt", "w") as f:
    for name in names:
        f.write(name)
[545]:
!type new_data.txt
SarahRachelBobBill

Il faut faire attention à inclure le caractère spécial "\n" pour marquer les sauts de ligne.

[546]:
names = ["Sarah", "Rachel", "Bob", "Bill"]
with open("new_data.txt", "w") as f:
    for name in names:
        f.write(name)
        f.write("\n")
[547]:
!type new_data.txt
Sarah
Rachel
Bob
Bill
[548]:
names = ["Sarah", "Rachel", "Bob", "Bill"]
with open("new_data.txt", "w") as f:
    for name in names:
        f.write(name + "\n")
[549]:
!type new_data.txt
Sarah
Rachel
Bob
Bill
[550]:
ages = [26, 24, 29, 25]
with open("new_data.txt", "w") as f:
    for age in ages:
        f.write(str(age) + "\n")
[551]:
!type new_data.txt
26
24
29
25

On peut aussi utiliser la méthode writelines pour écrire directement l’ensemble des éléments d’un objet dans le fichier.

[552]:
names = ["Sarah", "Rachel", "Bob", "Bill"]
with open("new_data.txt", "w") as f:
    f.writelines("\n".join(names))
[553]:
!type new_data.txt
Sarah
Rachel
Bob
Bill
[554]:
!del /f new_data.txt data.txt
%reset -f

Interactions


Le client est roi


On peut intéragir avec du code Python lors de son exécution de deux manières différentes: * En passant des arguments à la suite de la commande python myscript.py exécutée dans la Command Prompt. On peut récupérer la valeur de ces arguments et l’utiliser dans le code * En utilisant la fonction input qui met en pause l’exécution du code tant que l’utilisateur n’entre pas une valeur.

[555]:
%%writefile argv.py
import sys
age = sys.argv[1]
print(f"You are {age} years old.")
Writing argv.py
[556]:
# Equivalent à `python argv.py 28` dans la command prompt
%run argv.py 28
You are 28 years old.

Avec la première méthode, on peut facilement créer des outils flexibles sous la forme de scripts

[557]:
%%writefile add.py
import sys
a, b = sys.argv[1], sys.argv[2]  # ou a, b = sys.argv[1:]
print(float(a) + float(b))
Writing add.py
[558]:
%run add.py 3 4.5
7.5
[559]:
%%writefile showcontent.py
import sys
file = sys.argv[1]
with open(file, "r") as f:
    content = f.read()
print(content)
Writing showcontent.py
[560]:
%%writefile names.txt
Anna
Rachel
Bob
Bill
Writing names.txt
[561]:
%run showcontent.py names.txt
Anna
Rachel
Bob
Bill

La fonction input peut accepter un argument de type string. Si on lui en fournit un, elle l’affiche puis attent qu’on entre une valeur et tape sur la touche Entrée, l’objet qu’elle retourne est une string qui contient ce que l’on vient de taper.

[562]:
input()

[562]:
''
[572]:
age = input("How old are you?")
How old are you?25
[573]:
%%writefile input.py
age = input("How old are you?")
print(f"You are {age} years old.")
Writing input.py
[574]:
# Equivalent à `python input.py` dans la command prompt
%run input.py
How old are you?28
You are 28 years old.
[575]:
!del /f input.py argv.py showcontent.py names.txt add.py
%reset -f

Traceback


Suivie à la trace


Le traceback est le long message que Python affiche lorsqu’une erreur survient pendant l’exécution du code. Le traceback se lit de bas en haut. Il est très utile pour débugger un script.

[576]:
def first_level(a, b):
    return second_level(a, b)


def second_level(a, b):
    return third_level(a, b)


def third_level(a, b):
    return a + b
[577]:
first_level(1, 2)
[577]:
3

L’addition des paramètres a et b est réalisée par la fonction third_level. On va déclencher une TypeError en tentant d’additioner une string avec un int, ce qui est impossible.

[578]:
"Bob" + 1
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-578-1e6b406786db> in <module>
----> 1 "Bob" + 1

TypeError: can only concatenate str (not "int") to str
[579]:
first_level("Bob", 1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-579-26206af4b823> in <module>
----> 1 first_level("Bob", 1)

<ipython-input-576-0c177701bd8c> in first_level(a, b)
      1 def first_level(a, b):
----> 2     return second_level(a, b)
      3
      4
      5 def second_level(a, b):

<ipython-input-576-0c177701bd8c> in second_level(a, b)
      4
      5 def second_level(a, b):
----> 6     return third_level(a, b)
      7
      8

<ipython-input-576-0c177701bd8c> in third_level(a, b)
      8
      9 def third_level(a, b):
---> 10     return a + b

TypeError: can only concatenate str (not "int") to str

Le traceback nous indique que l’erreur s’est produite à la ligne return a + b du code dans la fonction third_level. Il afficher ensuite (de bas en haut) les appels successifs aux trois fonctions imbriquées.

[580]:
%reset -f

Classes


Des objets, des objets, des objets


On peut définir nos propres types d’objet avec le keyword class. La class définit les attributs et les méthodes des objets qu’elle va engendrer.

[581]:
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def __repr__(self):
        return f"rectangle of width: {self.width} and height: {self.height}"

    def __eq__(self, other_rectangle):
        if self.width == other_rectangle.width and self.height == other_rectangle.height:
            return True
        else:
            return False

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)
[582]:
type(Rectangle)
[582]:
type
[583]:
print(dir(Rectangle))
['__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__', 'area', 'perimeter']
[584]:
r1 = Rectangle(10, 5)
[585]:
type(r1)
[585]:
__main__.Rectangle
[586]:
print(dir(r1))
['__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__', 'area', 'height', 'perimeter', 'width']
[587]:
r1
[587]:
rectangle of width: 10 and height: 5
[588]:
print(r1.perimeter(), r1.area())
30 50
[589]:
r2 = Rectangle(5, 10)
r2 == r1
[589]:
False
[590]:
r1 > r2
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-590-e3bab7ac7782> in <module>
----> 1 r1 > r2

TypeError: '>' not supported between instances of 'Rectangle' and 'Rectangle'
[591]:
%reset -f
[ ]: