Table des matières

Semaine 1. Intro MOOC, outils Python

5. Notions de variables, d'objets et typage dynamique

Le nommage:

Les mots clés:

https://docs.python.org/3/reference/lexical_analysis.html#keywords

>>> x = 1
>>>type(x)
int
>>> isinstance(23, int)
True

Type hints

depuis python-3.5, il est possible d'ajouter des annotations de type, pour expliciter les suppositions qui sont faites par le programmeur pour le bon foncionnement du code.
A voir :
http://www.mypy-lang.org/
http://www.mypy-lang.org/examples.html

6. Les types numériques

Opérateurs Math.

48 / 5	division naturelle
// 	quotient
% 	modulo
** 	puissance

Conversions

Entier 		int
Flottant 	float
Complexe 	complex
Chaîne 		str

Entiers et bases

deux_cents = 0o310		# binaire
deux_cents = 0o310		# octale
deux_cents = 0xc8		# hexa
deux_cents = int('3020', 4)	# base 4

Fractions

# on importe le module fractions, qui lui-même définit le symbole Fraction
from fractions import Fraction
 
# et cette fois, les calculs sont exacts, et l'expression retourne bien True
Fraction(3, 10) - Fraction(1, 10) == Fraction(2, 10)
-> True
 
Fraction('0.3') - Fraction('0.1') == Fraction('2/10')
-> True

decimal

from decimal import Decimal
 
Decimal('0.3') - Decimal('0.1') == Decimal('0.2')
-> True

Opérations bitwise

Et logique : opérateur &

>>> x49 & y81
17
x49 		→ (0,1,1,0,0,0,1)
y81 		→ (1,0,1,0,0,0,1)
x49 & y81	→ (0,0,1,0,0,0,1) →16+1→17

Ou logique : opérateur |

>>> x49 | y81
113
x49		→(0,1,1,0,0,0,1)
y81		→(1,0,1,0,0,0,1)
x49 | y81	→(1,1,1,0,0,0,1)→64+32+16+1→113

Ou exclusif : opérateur ^

>>> x49 ^ y81
96

Décalages

>>> x49 << 4
784
x49	→(0,1,1,0,0,0,1)
x49<<4	→(0,1,1,0,0,0,1,0,0,0,0)→512+256+16→784
>>> x49 >> 4
3
x49	→(0,1,1,0,0,0,1)
x49>>4 	→(0,0,0,0,0,1,1)→2+1→3

Une astuce

>>> bin(x49)
'0b110001'  # chaîne de caractères

Pour en savoir plus :
https://docs.python.org/3/library/stdtypes.html#bitwise-operations-on-integer-types

Semaine 2. Notions de base pour écrire son premier programme en Python

1. Codage, jeux de caractères et Unicode

Entête spécifiant l'Encodage UTF-8

# -*- coding: <nom_de_l_encodage> -*-
ou
# coding: utf-8  #à utiliser de préférence

Connaître l'encodage du système

Pour les système GNU Linux :

>>> import sys
>>> print(sys.getdefaultencoding())
utf-8

Pour certain système Windows :

>>> import sys
>>> print(sys.getdefaultencoding())
cp1252

https://en.wikipedia.org/wiki/Windows-1252

Syntaxe de la ligne coding est précisée dans cette doc :
https://docs.python.org/3/reference/lexical_analysis.html#encoding-declarations
Et pour Python :
https://www.python.org/dev/peps/pep-0263/

2. Les chaînes de caractères

Les outils de base sur les strings

>>> help(str)

Décimal et chaîne de caractères

>>> '123'.isdecimal()
True

split et join

>>> 'abc=:=def=:=ghi=:=jkl'.split('=:=')
['abc', 'def', 'ghi', 'jkl']
>>> "=:=".join(['abc', 'def', 'ghi', 'jkl'])
'abc=:=def=:=ghi=:=jkl'

attention toutefois si le séparateur est un terminateur, la liste résultat contient alors une dernière chaîne vide. En pratique, on utilisera la méthode strip, que nous allons voir ci-dessous, avant la méthode split pour éviter ce problème.

>>> 'abc;def;ghi;jkl;'.strip(';')
'abc;def;ghi;jkl'
>>> ";".join(['abc', 'def', 'ghi', 'jkl', ''])
'abc;def;ghi;jkl;'

replace

>>> "abcdefabcdefabcdef".replace("abc", "zoo", 2)
'zoodefzoodefabcdef'

Plusieurs appels à replace peuvent être chaînés comme ceci:

>>> "les [x] qui disent [y]".replace("[x]", "chevaliers").replace("[y]", "Ni")
'les chevaliers qui disent Ni'

Nettoyage : strip

On pourrait par exemple utiliser replace pour enlever les espaces dans une chaîne.
Toutefois bien souvent on préfère utiliser strip qui ne s'occupe que du début et de la fin de la chaîne, et gère aussi les tabulations et autres retour à la ligne

>>> " \tune chaine avec des trucs qui dépassent \n".strip()
'une chaine avec des trucs qui dépassent'

On peut appliquer strip avant split pour éviter le problème du dernier élément vide.

>>> 'abc;def;ghi;jkl;'.strip(';').split(';')
['abc', 'def', 'ghi', 'jkl']

Rechercher une sous-chaîne

>>> # l'indice du début de la première occurrence
>>> "abcdefcdefghefghijk".find("def")
3
>>> # ou -1 si la chaine n'est pas présente:
>>> "abcdefcdefghefghijk".find("zoo")
-1

rfind fonctionne comme find mais en partant de la fin de la chaîne

>>> # en partant de la fin
>>> "abcdefcdefghefghijk".rfind("fgh")
13
>>> # notez que le résultat correspond
# tout de même toujours au début de la chaine
>>> "abcdefcdefghefghijk"[13]
'f'

Avec exception: Index

try:
    "abcdefcdefghefghijk".index("zoo")
except Exception as e:
    print("OOPS", type(e), e)
-> OOPS <class 'ValueError'> substring not found

Mais le plus simple pour chercher si une sous-chaîne est dans une autre chaîne est d'utiliser l'instruction in

>>> "def" in "abcdefcdefghefghijk"
True

La méthode count compte le nombre d'occurrences d'une sous-chaîne

>>>"abcdefcdefghefghijk".count("ef")
3

Début et fin de chaîne :

>>> "abcdefcdefghefghijk".startswith("abcd")
True
>>> "abcdefcdefghefghijk".endswith("ghijk")
True

Capitalisation

>>> "monty PYTHON".upper()
'MONTY PYTHON'
>>> "monty PYTHON".lower()
'monty python'
>>> "monty PYTHON".swapcase()
'MONTY python'
>>> "monty PYTHON".capitalize()
'Monty python'
>>> "monty PYTHON".title()
'Monty Python'

Pour en savoir plus

Formatage

>>> n = 'Thomas'
>>> age = 16
>>> "{} a {} ans".format(n, age)
'Thomas a 16 ans'
>>> "{0} a {1} ans".format(n, age)
'Thomas a 16 ans'
>>> "{le_prenom} a {l_age} ans".format(le_prenom=n, l_age=age)
'Thomas a 16 ans'
>>> "{n} a {age} ans".format(n=n, age=age)
Depuis la version Python 3.6 on peut utiliser les f-string
>>> n = 'Thomas'
>>> age = 16
>>> f"{n} a {age} ans"
'Thomas a 16 ans'
Ancien format: Opérateur %s
>>> # l'ancienne façon de formatter les chaînes avec %
>>> # est souvent moins lisible
>>> "%s %s a %s ans" % (prenom, nom, age)
'Jean Dupont a 35 ans'
 
>>> variables =  {'le_nom' : nom, 'le_prenom' : prenom, 'l_age' : age}
>>> "%(le_nom)s, %(le_prenom)s, %(l_age)s ans" % variables
'Dupont, Jean, 35 ans'

Précision des arrondis

>>> from math import pi
>>> # un f-string
>>> f"pi avec seulement 2 chiffres apres la virgule {pi:.2f}"
'pi avec seulement 2 chiffres apres la virgule 3.14'
>>> # avec format avec liaison par nom
>>> "pi avec seulement 2 chiffres apres la virgule {flottant:.2f}".format(flottant=pi)
'pi avec seulement 2 chiffres apres la virgule 3.14'

0 en début de nombre

>>> x = 15
>>> f"{x:04d}"

Ici on utilise le format d (toutes ces lettres d, f, g viennent des formats ancestraux de la libc comme printf). Ici avec :04d on précise qu'on veut une sortie sur 4 caractères et qu'il faut remplir avec des 0.

Largeur fixe

# les données à afficher
comptes = [
 ('Apollin', 'Dupont', 127),
 ('Myrtille', 'Lamartine', 25432),
 ('Prune', 'Soc', 827465),
]
 
for prenom, nom, solde in comptes:
    print(f"{prenom:<10} -- {nom:^12} -- {solde:>8} €")
Apollin    --    Dupont    --      127 €
Myrtille   --  Lamartine   --    25432 €
Prune      --     Soc      --   827465 €

boucle for

print(10*"=")
==========

Voir aussi

Nous vous invitons à vous reporter à la documentation de format pour plus de détails sur les formats disponibles, et notamment aux nombreux exemples qui y figurent.
https://docs.python.org/3/library/string.html#formatstrings https://docs.python.org/3/library/string.html#format-examples

Expressions régulières et le module re

findall

On se donne deux exemples de chaînes

sentences = ['Lacus a donec, vitae gravida proin sociis.', 
             'Neque ipsum! rhoncus cras quam.']
</file python>
On peut chercher tous les mots se terminant par a ou m dans une chaîne avec findall
<file python>
for sentence in sentences:
    print(f"---- dans >{sentence}<")
    print(re.findall(r"\w*[am]\W", sentence))

Regex : r“\w*[am]\W”

  • \w* : on veut trouver une sous-chaîne qui commence par un nombre quelconque, y compris nul (*) de caractères alphanumériques (\w). Ceci est défini en fonction de votre LOCALE.
  • [am] : immédiatement après, il nous faut trouver un caractère a ou m.
  • \W : et enfin, il nous faut un caractère qui ne soit pas alphanumérique. Ceci est important puisqu'on cherche les mots qui se terminent par un a ou un m, si on ne le mettait pas on obtiendrait ceci

split

Une autre forme simple d'utilisation des regexps est re.split, qui fournit une fonctionnalité voisine de str.split, mais ou les séparateurs sont exprimés comme une expression régulière

or sentence in sentences:
    print(f"---- dans >{sentence}<")
    print(re.split(r"\W+", sentence))
    print()
 
---- dans >Lacus a donec, vitae gravida proin sociis.<
['Lacus', 'a', 'donec', 'vitae', 'gravida', 'proin', 'sociis', '']
 
---- dans >Neque ipsum! rhoncus cras quam.<
['Neque', 'ipsum', 'rhoncus', 'cras', 'quam', '']

sub

for sentence in sentences:
    print(f"---- dans >{sentence}<")
    print(re.sub(r"(\w+)", r"X\1Y", sentence))
    print()
 
---- dans >Lacus a donec, vitae gravida proin sociis.<
XLacusY XaY XdonecY, XvitaeY XgravidaY XproinY XsociisY.
 
---- dans >Neque ipsum! rhoncus cras quam.<
XNequeY XipsumY! XrhoncusY XcrasY XquamY.

match

Un deuxième exemple:

samples = ['890hj000nnm890',    # cette entrée convient
          '123abc456def789',   # celle-ci aussi
          '8090abababab879',   # celle-ci non
          ]
regexp1 = "[0-9]+[A-Za-z]+[0-9]+[A-Za-z]+[0-9]+"
for sample in samples:
    match = re.match(regexp1, sample)
    print(f"{sample:16s} → {match}")
890hj000nnm890   → <_sre.SRE_Match object; span=(0, 14), match='890hj000nnm890'>
123abc456def789  → <_sre.SRE_Match object; span=(0, 15), match='123abc456def789'>
8090abababab879  → None
# pour simplement visualiser si on a un match ou pas
def nice(match):
    # le retour de re.match est soit None, soit un objet match
    return "no" if match is None else "Match!"
 
# la même chose mais un peu moins encombrant
print(f"REGEXP={regexp1}\n")
for sample in samples:
    match = re.match(regexp1, sample)
    print(f"{sample:>16s} → {nice(match)}")

Nommer un morceau (un groupe)

>>> # on se concentre sur une entrée correcte
>>> haystack = samples[1]
>>> haystack
'123abc456def789'
 
>>> # la même regexp, mais on donne un nom au groupe de chiffres central
>>> regexp2 = "[0-9]+[A-Za-z]+(?P<needle>[0-9]+)[A-Za-z]+[0-9]+"
>>> print(re.match(regexp2, haystack).group('needle'))
456

Un troisième exemple :

regexp3 = "(?P<id>[0-9]+)[A-Za-z]+(?P<needle>[0-9]+)[A-Za-z]+(?P=id)"
 
print(f"REGEXP={regexp3}\n")
for sample in samples:
    match = re.match(regexp3, sample)
    print(f"{sample:>16s} → {nice(match)}")  
 
-> 
REGEXP=(?P<id>[0-9]+)[A-Za-z]+(?P<needle>[0-9]+)[A-Za-z]+(?P=id)
 
  890hj000nnm890 → Match!
 123abc456def789 → no
 8090abababab879 → no

Comment utiliser la librairie

  • ompiler une seule fois votre chaîne en un automate, qui est matérialisé par un objet de la classe re.RegexObject, en utilisant re.compile,
  • puis d'utiliser directement cet objet autant de fois que vous avez de chaînes.
# au lieu de faire comme ci-dessus:
# imaginez 10**6 chaînes dans samples
for sample in samples:
    match = re.match(regexp3, sample)
    print(f"{sample:>16s} → {nice(match)}")    
->
  890hj000nnm890 → Match!
 123abc456def789 → no
 8090abababab879 → no
# dans du vrai code on fera plutôt:
# on compile la chaîne en automate une seule fois
re_obj3 = re.compile(regexp3)
# ensuite on part directement de l'automate
for sample in samples:
    match = re_obj3.match(sample)
    print(f"{sample:>16s} → {nice(match)}")
->
  890hj000nnm890 → Match!
 123abc456def789 → no
 8090abababab879 → no

Cette deuxième version ne compile qu'une fois la chaîne en automate, et donc est plus efficace.

Les méthodes sur la classe RegexObject les méthodes les plus utiles sur un objet RegexObject sont :

  • match et search, qui cherchent un match soit uniquement au début (match) ou n'importe où dans la chaîne (search),
  • findall et split pour chercher toutes les occurences (findall) ou leur négatif (split),
  • sub (qui aurait pu sans doute s'appeler replace, mais c'est comme ça) pour remplacer les occurrences de pattern.

Exploiter le résultat Les méthodes disponibles sur la classe re.MatchObject sont documentées en détail ici.
https://docs.python.org/3/library/re.html#match-objects

# exemple
sample = "    Isaac Newton, physicist"
match = re.search(r"(\w+) (?P<name>\w+)", sample)

re et string pour retrouver les données d'entrée du match.

>>> match.string
'    Isaac Newton, physicist'
>>> match.re
re.compile(r'(\w+) (?P<name>\w+)', re.UNICODE)

group, groups, groupdict pour retrouver les morceaux de la chaîne d'entrée qui correspondent aux groupes de la regexp. On peut y accéder par rang, ou par nom (comme on l'a vu plus haut avec needle).

>>> match.groups()
('Isaac', 'Newton')
>>> match.group(1)
'Isaac'
>>> match.group('name')
'Newton'
>>> match.group(2)
'Newton'
>>> match.groupdict()
{'name': 'Newton'}

Comme on le voit pour l'accès par rang les indices commencent à 1 pour des raisons historiques (on peut déjà référencer \1 en sed depuis la fin des années 70).

On peut aussi accéder au groupe 0 comme étant la partie de la chaîne de départ qui a effectivement été filtrée par l'expression régulière, et qui peut tout à fait être au beau milieu de la chaîne de départ, comme dans notre exemple

>>> match.group(0)
'Isaac Newton'

expand permet de faire une espèce de str.format avec les valeurs des groupes.

>>> match.expand(r"last_name \g<name> first_name \1")
'last_name Newton first_name Isaac'

span pour connaître les index dans la chaîne d'entrée pour un groupe donné.

>>> begin, end = match.span('name')
>>> sample[begin:end]
'Newton'

3. Les séquences

Les slices

Slice sans pas

>>> chaine = "abcdefghijklmnopqrstuvwxyz" ; print(chaine)
abcdefghijklmnopqrstuvwxyz
>>> chaine[2:6]
'cdef'

Conventions de début et fin

  • les indices commencent comme toujours à zéro
  • le premier indice debut est inclus
  • le second indice fin est exclu
  • on obtient en tout fin-debut items dans le résultat

Ainsi ci-dessus le résultat contient 6 - 2 = 4 éléments.

>>> # chaine[a:b] + chaine[b:c] == chaine[a:c]
>>> chaine[0:3] + chaine[3:7] == chaine[0:7]
'abcdef'
>>> # et bien entendu c'est la même chose si on omet la deuxième borne
>>> chaine[24:]
'yz'
>>> # ou même omettre les deux bornes, auquel cas on
>>> # fait une copie de l'objet - on y reviendra plus tard
>>> chaine[:]
'abcdefghijklmnopqrstuvwxyz'

Indices négatifs On peut utiliser des indices négatifs pour compter à partir de la fin

>>> chaine[3:-3]
'defghijklmnopqrstuvw'
>>> chaine[-3:]
'xyz'

Slice avec pas

>>> # le pas est précisé après un deuxième deux-points (:)
>>> # ici on va choisir un caractère sur deux dans la plage [3:-3]
>>> chaine[3:-3:2]
'dfhjlnprtv'
>>> chaine[3:-4:2]
'dfhjlnprtv'

Pas négatif

>>> chaine[-3:3]
''
>>> chaine[-3:3:-2]
'xvtrpnljhf'

Listes

>>> liste = [0, 2, 4, 8, 16, 32, 64, 128]
>>> liste
[0, 2, 4, 8, 16, 32, 64, 128]
>>> liste[-1:1:-2]
[128, 32, 8]

Et même ceci, qui peut être déroutant. Nous reviendrons dessus.

>>> liste[2:4] = [100, 200, 300, 400, 500]
>>> liste
[0, 2, 100, 200, 300, 400, 500, 16, 32, 64, 128]

numpy

La librairie numpy permet de manipuler des tableaux ou matrices. En anticipant (beaucoup) sur son usage que nous reverrons bien entendu en détails, voici un aperçu de ce qu'on peut faire avec des slices sur des objets numpy.

# ces deux premières cellules sont à admettre
# on construit un tableau ligne
import numpy as np
 
un_cinq = np.array([1, 2, 3, 4, 5]) 
un_cinq
 
array([1, 2, 3, 4, 5])
# ces deux premières cellules sont à admettre
# on le combine avec lui-même - et en utilisant une slice un peu magique
# pour former un tableau carré 5x5
 
array = 10 * un_cinq[:, np.newaxis] + un_cinq
array
 
array([[11, 12, 13, 14, 15],
       [21, 22, 23, 24, 25],
       [31, 32, 33, 34, 35],
       [41, 42, 43, 44, 45],
       [51, 52, 53, 54, 55]])

Semaine 3. Renforcement des notions de base, références partagées

1. Les fichiers

glob

le module glob pour la recherche de fichiers, par exemple pour trouver tous les fichiers en *.txt
https://docs.python.org/3/library/glob.html

dirpath = Path('./data/')
# tous les fichiers *.json dans le répertoire data/
for json in dirpath.glob("*.json"):
    print(json)
 
data/cities_europe.json
data/cities_france.json
data/cities_idf.json
data/cities_world.json
data/marine-e1-abb.json
data/marine-e1-ext.json
data/marine-e2-abb.json
data/marine-e2-ext.json

la documentation:
https://docs.python.org/3/library/pathlib.html

2. Les tuples

Extended unpacking

reference = [1, 2, 3, 4, 5]
a, *b, c = reference
print(f"a={a} b={b} c={c}")
 
a=1 b=[2, 3, 4] c=5
reference = range(20)
a, *b, c = reference
print(f"a={a} b={b} c={c}")
 
a=0 b=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] c=19
# si on sait que data contient prenom, nom, et un nombre inconnu d'autres informations
data = [ 'Jean', 'Dupont', '061234567', '12', 'rue du chemin vert', '57000', 'METZ', ]
# on peut utiliser la variable _ qui véhicule l'idée qu'on ne s'y intéresse pas vraiment
prenom, nom, *_ = data
print(f"prenom={prenom} nom={nom}")
 
prenom=Jean nom=Dupont
entree = [1, 2, 3]
 
_, milieu, _ = entree
print('milieu', milieu)
 
ignored, ignored, right = entree
print('right', right)
 
milieu 2
right 3

Profondeur :

structure = ['abc', [(1, 2), ([3], 4)], 5]
(a, (b, ((trois,), c)), d) = structure
print('trois', trois)
 
trois 3
 
ou
 
a, (b, ([trois], c)), d) = structure
print('trois', trois)
 
trois 3
 
ou
 
trois = structure[1][1][0][0]
print('trois', trois)
 
trois 3

Extended unpacking et profondeur

# un exemple très alambiqué avec plusieurs variables *extended
tree = [1, 2, [(3, 33, 'three', 'thirty-three')], ( [4, 44, ('forty', 'forty-four')])]
*_,  ((_, *x3, _),), (*_, x4) = tree
print(f"x3={x3}, x4={x4}")
 
x3=[33, 'three'], x4=('forty', 'forty-four')

Pour en savoir plus
//www.python.org/dev/peps/pep-3132/)

for, zip, enumerate

Plusieurs variables dans une boucle for

entrees = [(1, 2), (3, 4), (5, 6)]
for a, b in entrees:
    print(f"a={a} b={b}")
 
a=1 b=2
a=3 b=4
a=5 b=6

La fonction zip

villes = ["Paris", "Nice", "Lyon"]
populations = [2*10**6, 4*10**5, 10**6]
list(zip(villes, populations))
 
[('Paris', 2000000), ('Nice', 400000), ('Lyon', 1000000)]
for ville, population in zip(villes, populations):
    print(population, "habitants à", ville)
 
2000000 habitants à Paris
400000 habitants à Nice
1000000 habitants à Lyon
for i, j, k in zip(range(3), range(100, 103), range(200, 203)):
    print(f"i={i} j={j} k={k}")
 
i=0 j=100 k=200
i=1 j=101 k=201
i=2 j=102 k=202
Remarque: lorsqu'on passe à zip des listes de tailles différentes, le résultat est tronqué, c'est l'entrée de plus petite taille qui détermine la fin du parcours.
# on n'itère que deux fois 
# car le premier argument de zip est de taille 2
for units, tens in zip( [1, 2], [10, 20, 30, 40]):
    print(units, tens)
 
1 10
2 20

La fonction enumerate

villes = ["Paris", "Nice", "Lyon"]
populations = [2*10**6, 4*10**5, 10**6]
 
for i, ville in enumerate(villes):
    print(i, ville)
 
0 Paris
1 Nice
2 Lyon

4. Les dictionnaires

Construction par affectation direct:

annuaire = dict(marc=35, alice=30, eric=38)
print(annuaire)
{'marc': 35, 'alice': 30, 'eric': 38}

Lecture d'une clé:

print('la valeur pour marc est', annuaire['marc'])
la valeur pour marc est 35

Si on est pas sur de la clé:

print('valeur pour marc', annuaire.get('marc', 0))
print('valeur pour inconnu', annuaire.get('inconnu', 0))  # accepte valeur par défaut
valeur pour marc 35
valeur pour inconnu 0

Ajouter une clé:

annuaire['bob'] = 42
print(annuaire)
{'marc': 35, 'alice': 30, 'eric': 39, 'bob': 42}

Suppression d'une clé:

del annuaire['marc']
print(annuaire)
{'alice': 30, 'eric': 39, 'bob': 42}

Pour savoir si une clé existe:

print('john' in annuaire)
False

Construction d'un dictionnaire via un liste de tuple:

liste = [('ana', 35), ('eve', 30), ('bob', 38)]
age = dict(liste)
age
{'ana': 35), ('eve': 30), ('bob': 38}

Effacer une entrée:

del age['bob']

Exemple commun avec les liste et autre

len(age)
'ana' in age

Spécifique au dictionnaire:

age.keys()
-> dict_keys(['ana', 'eve'])
 
age.values()
-> dict_values([35, 30])
 
age.items()
dict_items([('ana', 35), ('eve', 30)])
Retourne des Vues qui sont misent à jour en même temps que le dictionnaire

Boucle for utilisant le unpacking (tuple):

for k, v in age.items():
    print(f"{k} {v}")
 
ana 35
eve 30
bob 25

Boucle for sans préciser de vue: retourne les clées

for k in age:
    print(k)
 
ana
eve
bob

La méthode update

print(f"avant: {list(annuaire.items())}")
avant: [('alice', 30), ('eric', 39), ('bob', 42)]
annuaire.update({'jean':25, 'eric':70})
list(annuaire.items())
[('alice', 30), ('eric', 70), ('bob', 42), ('jean', 25)]

Module complémentaire pour ordonner un dictionnaire:

from collections import OrderedDict
d = OrderedDict()
for i in ['a', 7, 3, 'x']: 
    d[i] = i
for k, v in d.items():
    print('OrderedDict', k, v)
OrderedDict a a
OrderedDict 7 7
OrderedDict 3 3
OrderedDict x x

obtenir séparément la liste des clés et des valeurs

for clé in annuaire.keys():
    print(clé)
alice
eric
bob
for valeur in annuaire.values():
    print(valeur)
30
39
42

Pour en savoir plus:
https://docs.python.org/3/library/stdtypes.html#mapping-types-dict

Niveau intermédiaire

Imaginons que vous devez gérer un dictionnaire dont les valeurs sont des listes, et que votre programme ajoute des valeurs au fur et à mesure dans ces listes.

Avec un dictionnaire de base, cela peut vous amener à écrire un code qui ressemble à ceci:

# on lit dans un fichier des couples (x, y)
 
tuples = [
    (1, 2),
    (2, 1),
    (1, 3),
    (2, 4),
]
# et on veut construire un dictionnaire
# x -> [ liste de tous les y connectés à x]
resultat = {}
 
for x, y in tuples:
    if x not in resultat:
        resultat[x] = []
    resultat[x].append(y)
 
for key, value in resultat.items():
    print(key, value)
1 [2, 3]
2 [1, 4]

Cela fonctionne, mais n'est pas très élégant. Pour simplifier ce type de traitements, vous pouvez utiliser defaultdict, une sous-classe de dict dans le module collections:

from collections import defaultdict
 
# on indique que les valeurs doivent être créés à la volée
# en utilisant la fonction list
resultat = defaultdict(list)
 
# du coup plus besoin de vérifier la présence de la clé
for x, y in tuples:
    resultat[x].append(y)
 
for key, value in resultat.items():
    print(key, value)    
1 [2, 3]
2 [1, 4]

Cela fonctionne aussi avec le type int, lorsque vous voulez par exemple compter des occurrences:

compteurs = defaultdict(int)

phrase = "une phrase dans laquelle on veut compter les caractères"

for c in phrase:
    compteurs[c] += 1
    
sorted(compteurs.items())
[(' ', 8),
 ('a', 5),
 ('c', 3),
 ('d', 1),
 ('e', 8),
 ('h', 1),
 ('l', 4),
 ('m', 1),
 ('n', 3),
 ('o', 2),
 ('p', 2),
 ('q', 1),
 ('r', 4),
 ('s', 4),
 ('t', 3),
 ('u', 3),
 ('v', 1),
 ('è', 1)]

niveau avancé

d = {'a' : 1, 'b' : 2}
keys = d.keys()
keys
-> dict_keys(['a', 'b'])
for key in keys:
    print(key)
a
b
print('a' in keys)
True
print('x' in keys)
False

Mais ce ne sont pas des listes

isinstance(keys, list)
False

Ce qui signifie qu'on n'a pas alloué de mémoire pour stocker toutes les clés, mais seulement un objet qui ne prend pas de place, ni de temps à construire:

# construisons un dictionnaire 
# pour anticiper un peu sur la compréhension de dictionnaire
 
big_dict = {k : k**2 for k in range(1_000_000)}
%%timeit -n 10000 
# créer un objet vue est très rapide
big_keys = big_dict.keys()

208 ns ± 21.8 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

# on répète ici car timeit travaille dans un espace qui lui est propre
# et donc on n'a pas défini big_keys pour notre interpréteur
big_keys = big_dict.keys()
%%timeit -n 20
# si on devait vraiment construire la liste ce serait beaucoup plus long
big_lkeys = list(big_keys)
16.4 ms ± 1.03 ms per loop (mean ± std. dev. of 7 runs, 20 loops each)

En fait ce sont des vues
Une autre propriété un peu inattendue de ces objets, c'est que ce sont des vues; ce qu'on veut dire par là (pour ceux qui connaissent, cela fait fait référence à la notion de vue dans les bases de données) c'est que la vue voit les changements fait sur l'objet dictionnaire même après sa création:

d = {'a' : 1, 'b' : 2}
keys = d.keys()
# sans surprise, il y a deux clés dans keys
for k in keys: 
    print(k)
a
b

keys et rafraîchi en temps réel :

# mais si maintenant j'ajoute un objet au dictionnaire
d['c'] = 3
# alors on va 'voir' cette nouvelle clé à partir de l'objet keys 
# qui pourtant est inchangé 
for k in keys: 
    print(k)
a
b
c

Clés immuables

Une clé doit être globalement immuable

Type Mutable ?
int, float immuable
complex,bool immuable
str immuable
list mutable
dict mutable
set mutable
frozenset immuable

struct/record: dictionnaires par compréhension

personnes = [
   {'nom': 'pierre',  'age': 25, 'email': 'pierre@foo.com'},
   {'nom': 'paul',    'age': 18, 'email': 'paul@bar.com'},
   {'nom': 'jacques', 'age': 52, 'email': 'jacques@cool.com'},
]
# on crée un index permettant de retrouver rapidement
# une personne dans la liste
index_par_nom = {personne['nom']: personne for personne in personnes}
 
print("enregistrement pour pierre", index_par_nom['pierre'])
enregistrement pour pierre {'nom': 'pierre', 'age': 26, 'email': 'pierre@foo.com'}
for nom, record in index_par_nom.items():
    print(f"Nom : {nom} -> enregistrement : {record}")
Nom : pierre -> enregistrement : {'nom': 'pierre', 'age': 26, 'email': 'pierre@foo.com'}
Nom : paul -> enregistrement : {'nom': 'paul', 'age': 18, 'email': 'paul@bar.com'}
Nom : jacques -> enregistrement : {'nom': 'jacques', 'age': 52, 'email': 'jacques@cool.com'}

La même idée, mais avec une classe Personne

class Personne:
 
    # le constructeur - vous ignorez le paramètre self,
    # on pourra construire une personne à partir de 
    # 3 paramètres
    def __init__(self, nom, age, email):
        self.nom = nom
        self.age = age
        self.email = email
 
    # je définis cette méthode pour avoir
    # quelque chose de lisible quand je print()
    def __repr__(self):
        return f"{self.nom} ({self.age} ans) sur {self.email}"

Pour construire la liste de personnes:

personnes2 = [
    Personne('pierre', 25, 'pierre@foo.com'),
    Personne('paul', 18, 'paul@bar.com'),
    Personne('jacques', 52, 'jacques@cool.com'),
]
personnes2[0]
pierre (25 ans) sur pierre@foo.com

Je peux indexer tout ceci comme tout à l'heure, si j'ai besoin d'un accès rapide:

# je dois utiliser cette fois personne.nom et non plus personne['nom']
index2 = {personne.nom : personne for personne in personnes2}
print(index2['pierre'])
pierre (25 ans) sur pierre@foo.com

Set

le type set est un type mutable

ensemble = {1, 2, 1}
ensemble
{1, 2}
# pour nettoyer
ensemble.clear()
ensemble
set()
# ajouter un element
ensemble.add(1)
ensemble
{1}
# ajouter tous les elements d'un autre *ensemble*
ensemble.update({2, (1, 2, 3), (1, 3, 5)})
ensemble
{(1, 2, 3), (1, 3, 5), 1, 2}
# enlever un element avec discard
ensemble.discard((1, 3, 5))
ensemble
{(1, 2, 3), 1, 2}
# discard fonctionne même si l'élément n'est pas présent
ensemble.discard('foo')
ensemble
{(1, 2, 3), 1, 2}

Exception

def function_with_else(number):
    try:
        x = 1/number
    except ZeroDivisionError as e:
        print(f"OOPS, {type(e)}, {e}")
    else:
        print("on passe ici seulement avec un nombre non nul")
    return 'something else'

[...]

[…]

Semaine 4. Fonctions et portée des variables

1. Fonctions

[...]

[…]

Type hints

docs.python.org/fr/3.6/library/typing
typer une variable:

nb_items : int = 0

Usages:
À ce stade, on peut entrevoir les usages suivants à ce type d'annotation :

  • tout d'abord, et évidemment, cela peut permettre de mieux documenter le code ;
  • les environnements de développement sont susceptibles de vous aider de manière plus effective ; si quelque part vous écrivez z = fact(12), le fait de savoir que z est entier permet de fournir une complétion plus pertinente lorsque vous commencez à écrire z.[TAB] ;
  • on peut espérer trouver des erreurs dans les passages d'arguments à un stade plus précoce du développement.

Par contre ce qui est très très clairement annoncé également, c'est que ces informations de typage sont totalement facultatives, et que le langage les ignore totalement.

# l'interpréteur ignore totalement ces informations
def fake_fact(n : str) -> str:
    return 1 if n <= 1 else n * fake_fact(n-1)
 
# on peut appeler fake_fact avec un int alors 
# que c'est déclaré pour des str
fake_fact(12)
479001600

Le modèle préconisé est d'utiliser des outils extérieurs, qui peuvent faire une analyse statique du code pour exploiter ces informations à des fins de validation. Dans cette catégorie, le plus célèbre est sans doute `mypy`http://mypy-lang.org/. Notez aussi que les IDE comme PyCharm sont également capables de tirer parti de ces annotations.

Comment annoter son code

from typing import List
# une fonction qui 
# attend un paramètre qui soit une liste d'entiers,
# et qui retourne une liste de chaines
def foo(x: List[int]) -> List[str]:
    pass    
 
 
from typing import Iterable
def lower_split(sep: str, inputs : Iterable[str]) -> str:
    return sep.join([x.lower() for x in inputs])
 
lower_split('--', ('AB', 'CD', 'EF'))
'ab--cd--ef'

un exemple plus complet

from typing import Dict, Tuple, List
 
ConnectionOptions = Dict[str, str]
Address = Tuple[str, int]
Server = Tuple[Address, ConnectionOptions]
 
def broadcast_message(message: str, servers: List[Server]) -> None:
    ...
 
# The static type checker will treat the previous type signature as
# being exactly equivalent to this one.
def broadcast_message(
        message: str,
        servers: List[Tuple[Tuple[str, int], Dict[str, str]]]) -> None:
    ...

aliases

from typing import NewType
 
UserId = NewType('UserId', int)
 
user1_id : UserId = 0
 
user1_id : int = 0

Complément - niveau avancé

from typing import TypeVar, Generic
from logging import Logger
 
T = TypeVar('T')
 
class LoggedVar(Generic[T]):
    def __init__(self, value: T, name: str, logger: Logger) -> None:
        self.name = name
        self.logger = logger
        self.value = value
 
    def set(self, new: T) -> None:
        self.log('Set ' + repr(self.value))
        self.value = new
 
    def get(self) -> T:
        self.log('Get ' + repr(self.value))
        return self.value
 
    def log(self, message: str) -> None:
        self.logger.info('%s: %s', self.name, message)

https://docs.python.org/3/library/typing.html#user-defined-generic-types

2. if/elif/else

x = True  # ou quoi que ce soit d'autre
if x:
    y = 12
else:
    y = 35
print(y)
12

Expression conditionnelle

y = 12 if x else 35
print(y)
12

Imbrications

# on veut calculer en fonction d'une entrée x
# une sortie qui vaudra
# -1 si x < -10
# 0 si -10 <= x <= 10
# 1 si x > 10
 
x = 5 # ou quoi que ce soit d'autre
 
valeur = -1 if x < -10 else (0 if x <= 10 else 1)
 
print(valeur)
0

Remarquez bien que cet exemple est équivalent à la ligne

valeur = -1 if x < -10 else 0 if x <= 10 else 1

https://docs.python.org/3/reference/expressions.html#conditional-expressions

def show_bool(x):
    print(f"condition {repr(x):>10} considérée comme {bool(x)}")
 
for exp in [None, "", 'a', [], [1], (), (1, 2), {}, {'a': 1}, set(), {1}]:
    show_bool(exp)
 
condition       None considérée comme False
condition         '' considérée comme False
condition        'a' considérée comme True
condition         [] considérée comme False
condition        [1] considérée comme True
condition         () considérée comme False
condition     (1, 2) considérée comme True
condition         {} considérée comme False
condition   {'a': 1} considérée comme True
condition      set() considérée comme False
condition        {1} considérée comme True
Famille Exemples
Égalité ==, !=, is, is not
Appartenance in
Comparaison ⇐, <, >, >=
Logiques and, or, not

4. Portée des Variables

Une variable de boucle reste définie au-delà de la boucle

# La variable 'i' n'est pas définie
try:
    i
except NameError as e:
    print('OOPS', e)
OOPS name 'i' is not defined
# si à présent on fait une boucle
# avec i comme variable de boucle
for i in [0]:
    pass
 
# alors maintenant i est définie
i
0

On dit que la variable fuite (en anglais “leak”), dans ce sens qu'elle continue d'exister au delà du bloc de la boucle à proprement parler.

5. Modification de la portée avec global et nonlocal

Pour voir un module:

from modtools import show_module
show_module(nom_du_module)

6. Passage d'arguments et appel de fonctions

Dans le cas général, il est possible de combiner les 4 formes d'arguments:

  • arguments “normaux”, dits positionnels
  • arguments nommés, comme nom=<valeur>
  • forme *args (tuple unpacking)
  • forme * * dargs (dictionnaire)
  1. paramètres positionnels (usuels)
  2. paramètres nommés (forme name=default)
  3. paramètres *args qui attrape dans un tuple le relicat des arguments positionnels
  4. paramètres * * kwds qui attrape dans un dictionnaire le relicat des arguments nommés

Écrire un wrapper

def ligne_rouge(*arguments, **keywords):
    # c'est le seul endroit où on fait une hypothèse sur la fonction `ligne`
    # qui est qu'elle accepte un argument nommé 'couleur'
    keywords['couleur'] = "rouge"
    return ligne(*arguments, **keywords)
 
ligne_rouge(0, 100, 100, 0, epaisseur=4)
la ligne (0, 100) -> (100, 0) en rouge - ep. 4

N'utilisez pas d'objet mutable pour les valeurs par défaut

# ne faites SURTOUT PAS ça
def ne_faites_pas_ca(options={}):
    "faire quelque chose"
# mais plutôt comme ceci
def mais_plutot_ceci(options=None):
    if options is None: 
        options = {}
    "faire quelque chose"

Semaine 5. Itération, importation et espace de nommage

1. Itérable, itérateur, itération

Une limite de la boucle for

# on veut enlever de l'ensemble toutes les chaines 
# qui ne contiennent pas par 'bert'
ensemble = {'marc', 'albert'}
 
# ceci semble une bonne idée mais ne fonctionne pas
for valeur in ensemble:
    if 'bert' not in valeur:
        ensemble.discard(valeur)
 
RuntimeError ....

Première remarque, votre premier réflexe pourrait être de penser à une compréhension d'ensemble :

ensemble2 = {valeur for valeur in ensemble if 'bert' in valeur}
ensemble2
{'albert'}

'est sans doute la meilleure solution. Par contre, évidemment, on n'a pas modifié l'objet ensemble initial, on a créé un nouvel objet. En supposant que l'on veuille modifier l'objet initial, il nous faut faire la boucle sur une shallow copy de cet objet. Notez qu'ici, il ne s'agit d'économiser de la mémoire, puisque l'on fait une shallow copy.

from copy import copy
# on veut enlever de l'ensemble toutes les chaines 
# qui ne contiennent pas 'bert'
ensemble = {'marc', 'albert'}
 
# si on fait d'abord une copie tout va bien
for valeur in copy(ensemble):
    if 'bert' not in valeur:
        ensemble.discard(valeur)
 
print(ensemble)
{'albert'}
Avertissement
Dans l'exemple ci-dessus, on voit que l'interpréteur se rend compte que l'on est en train de modifier l'objet de la boucle, et nous le signifie.

Ne vous fiez pas forcément à cet exemple, il existe des cas – nous en verrons plus loin dans ce document – où l'interpréteur peut accepter votre code alors qu'il n'obéit pas à cette règle, et du coup essentiellement se mettre à faire n'importe quoi.

Précisons bien la limite Pour être tout à fait clair, lorsqu'on dit qu'il ne faut pas modifier l'objet de la boucle for, il ne s'agit que du premier niveau.

On ne doit pas modifier la composition de l'objet en tant qu'itérable, mais on peut sans souci modifier chacun des objets qui constitue l'itération.

Ainsi cette construction par contre est tout à fait valide :

liste = [[1], [2], [3]]
print('avant', liste)
avant [[1], [2], [3]]
for sous_liste in liste:
    sous_liste.append(100)
print('après', liste)
après [[1, 100], [2, 100], [3, 100]]

Dans cet exemple, les modifications ont lieu sur les éléments de liste, et non sur l'objet liste lui-même, c'est donc tout à fait légal.

Difficulté d'implémentation

# cette boucle ne termine pas
liste = [1, 2, 3]
for c in liste:
    if c == 3:
        liste.append(c)

Itérateurs

Le module itertools

https://docs.python.org/fr/3/library/itertools.html

import itertools
for x in itertools.chain((1, 2), [3, 4]):
    print(x)
1
2
3
4

2. Objet fonction, fonction lambda, map et filter

import string
support = string.ascii_lowercase
print(f'support={support}')
support=abcdefghijklmnopqrstuvwxyz
# range
for x in range(3, 8):
    print(x)
3
4
5
6
7
# islice
for x in itertools.islice(support, 3, 8):
    print(x)
d
e
f
g
h

reduce

from functools import reduce
entrees = [8, 5, 12, 4, 45, 7]
 
print('sum', sum(entrees))
print('min', min(entrees))
print('max', max(entrees))
sum 81
min 4
max 45

Tri de listes

Dans le cas général, on est souvent amené à trier des objets selon un critère propre à l'application. Imaginons par exemple que l'on dispose d'une liste de tuples à deux éléments, dont le premier est la latitude et le second la longitude :

coordonnees = [(43, 7), (46, -7), (46, 0)]
 
def longitude(element): 
    return element[1]
 
coordonnees.sort(key=longitude)
print("coordonnées triées par longitude", coordonnees)
coordonnées triées par longitude [(46, -7), (46, 0), (43, 7)]

On aurait pu utiliser de manière équivalente une fonction lambda ou la methode itemgetter to module operator

# fonction lambda 
coordonnees = [(43, 7), (46, -7), (46, 0)]
coordonnees.sort(key=lambda x: x[1])
print("coordonnées triées par longitude", coordonnees)
 
# méthode operator.getitem
import operator
coordonnees = [(43, 7), (46, -7), (46, 0)]
coordonnees.sort(key=operator.itemgetter(1))
print("coordonnées triées par longitude", coordonnees)
coordonnées triées par longitude [(46, -7), (46, 0), (43, 7)]
coordonnées triées par longitude [(46, -7), (46, 0), (43, 7)]

Fonction de commodité : sorted

On a vu que sort réalise le tri de la liste “en place”. Pour les cas où une copie est nécessaire, python fournit également une fonction de commodité, qui permet précisément de renvoyer la copie triée d'une liste d'entrée. Cette fonction est baptisée sorted, elle s'utilise par exemple comme ceci, sachant que les arguments reverse et key peuvent être mentionnés comme avec sort :

liste = [8, 7, 4, 3, 2, 9, 1, 5, 6]
# on peut passer à sorted les mêmes arguments que pour sort
triee = sorted(liste, reverse=True)
# nous avons maintenant deux objets distincts
print('la liste triée est une copie ', triee)
print('la liste initiale est intacte', liste)
la liste triée est une copie  [9, 8, 7, 6, 5, 4, 3, 2, 1]
la liste initiale est intacte [8, 7, 4, 3, 2, 9, 1, 5, 6]

Nous avons qualifié sorted de fonction de commodité car il est très facile de s'en passer ; en effet on aurait pu écrire à la place du fragment précédent :

liste = [8, 7, 4, 3, 2, 9, 1, 5, 6]
# ce qu'on a fait dans la cellule précédente est équivalent à
triee = liste[:]
triee.sort(reverse=True)
# 
print('la liste triée est une copie ', triee)
print('la liste initiale est intacte', liste)
a liste triée est une copie  [9, 8, 7, 6, 5, 4, 3, 2, 1]
la liste initiale est intacte [8, 7, 4, 3, 2, 9, 1, 5, 6]

Alors que sort est une fonction sur les listes, sorted peut trier n'importe quel itérable et retourne le résultat dans une liste. Cependant, au final, le coût mémoire est le même. Pour utiliser sort on va créer une liste des éléments de l'itérable, puis on fait un tri en place avec sort. Avec sorted on applique directement le tri sur l'itérable, mais on crée une liste pour stocker le résultat. Dans les deux cas, on a une liste à la fin et aucune structure de données temporaire créée.
Pour en savoir plus:
https://docs.python.org/3/howto/sorting.html

3. Compréhension de listes, sets et dictionnaires

Compréhension de listes

Créer une liste des prénoms qui commence par 'a' et les mettre en minuscule:

prenoms = ['ana', 'eve', 'ALICE', 'Anne', 'bob']
a_prenoms = [p.lower() for p in prenoms if p.lower().startswith('a') ]
a_prenoms
['ana', 'alice', 'anne']

Compréhension de sets

prenoms = ['ana', 'eve', 'ALICE', 'Anne', 'bob']
# on double volontairement les prénoms
prenoms = prenoms.extend(prenoms)
prenoms
['ana', 'eve', 'ALICE', 'Anne', 'bob','ana', 'eve', 'ALICE', 'Anne', 'bob']
# on recré la liste
a_prenoms = [p.lower() for p in prenoms if p.lower().startswith('a') ]
a_prenoms
['ana', 'alice', 'anne','ana', 'alice', 'anne']
# on voudrait supprimer les doublons en utilisant une compréhension de set
a_prenoms = {p.lower() for p in prenoms if p.lower().startswith('a') }
a_prenoms
{'ana', 'alice', 'anne'}

Compréhension de dictionnaires

ages = [ ('ana', 20), ('EVE', 30), ('bob', 40) ]
# conversion en dictionnaire
ages = dict(ages)
ages
{'ana': 20, 'EVE': 30, 'bob': 40}
# on extrait les prenoms en minuscule 
ages_fix = { p.lower():a for p, a in ages.items() }
ages_fix
{'ana': 20, 'eve': 30, 'bob': 40}
# on extrait les prenoms en minuscule dont les ages sont inférieur à 40
ages_fix = { p.lower():a for p, a in ages.items() if a < 40 }
ages_fix
{'ana': 20, 'eve': 30}

Tracer une courbe avec matplotlib

Sous ipython jupyter

depart = (-5, -3, 0, 3, 5, 10)
arrivee = [x**2 for x in depart]
arrivee
[25, 9, 0, 9, 25, 100]
 
%matplotlib inline
import matplotlib.pyplot as plt
plt.ion()
# si on met le depart et l'arrivee 
# en abscisse et en ordonnee, on trace
# une version tronquée de la courbe de f: x -> x**2
plt.plot(depart, arrivee);

Pour en savoir plus sur les compréhensions de liste
https://docs.python.org/fr/3/tutorial/datastructures.html#list-comprehensions

Compréhensions imbriquées

On peut également imbriquer plusieurs niveaux pour ne construire qu'une seule liste, comme par exemple :

# liste de profondeur 1
[n + p for n in [2, 4] for p in [10, 20, 30]]
[12, 22, 32, 14, 24, 34]
# liste de profondeur 2
[[n + p for n in [2, 4]] for p in [10, 20, 30]]
[[12, 14], [22, 24], [32, 34]]

Ordre d'évaluation de [[ .. for .. ] .. for .. ]

n = 4
[[(i, j) for i in range(1, j + 1)] for j in range(1, n + 1)]
[[(1, 1)],
 [(1, 2), (2, 2)],
 [(1, 3), (2, 3), (3, 3)],
 [(1, 4), (2, 4), (3, 4), (4, 4)]]

Et dans ce cas, très logiquement, l'évaluation se fait en commençant par la fin, ou si on préfère “par l'extérieur”, c'est-à-dire que le code ci-dessus est équivalent à :

# en version bavarde, pour illustrer l'ordre des "for"
resultat_exterieur = []
for j in range(1, n + 1):
    resultat_interieur = []
    for i in range(1, j + 1):
        resultat_interieur.append((i, j))
    resultat_exterieur.append(resultat_interieur)
resultat_exterieur
[[(1, 1)],
 [(1, 2), (2, 2)],
 [(1, 3), (2, 3), (3, 3)],
 [(1, 4), (2, 4), (3, 4), (4, 4)]]

Avec if

Lorsqu'on assortit les compréhensions imbriquées de cette manière de clauses if, l'ordre d'évaluation est tout aussi logique. Par exemple, si on voulait se limiter - arbitrairement - aux lignes correspondant à j pair, et aux diagonales où i+j est pair, on écrirait :

[[(i, j) for i in range(1, j + 1) if (i + j)%2 == 0]
         for j in range(1, n + 1) if j % 2 == 0]
[[(2, 2)], [(2, 4), (4, 4)]]

ce qui est équivalent à :

# en version bavarde à nouveau
resultat_exterieur = []
for j in range(1, n + 1):
    if j % 2 == 0:
        resultat_interieur = []
        for i in range(1, j + 1):
            if (i + j) % 2 == 0:
                resultat_interieur.append((i, j))
        resultat_exterieur.append(resultat_interieur)
resultat_exterieur
[[(2, 2)], [(2, 4), (4, 4)]]

4. Expressions et fonctions génératrices

Expressions génératrices

C'est comme une compréhension de liste :

  • Ne retourne pas une liste (souvent utilisé temporairement)
  • Retourne un itérateur qui fera les calculs à la volée lors de l'itération.

Exemple, somme des carré: avec un compréhension de liste:

carre = [x**2 for x in range(1000)]  # liste temporaire
sum(carre)
332833500

avec une expression génératrice (generator object):

carre = (x**2 for x in range(1000)) # expression génératrice
sum(carre)
332833500
sum(carre) # le 2ème appel retourne 0, normal car c'est un itérateur qui a été consommé
0
next(carre)
StopIteration ...

Exemple d'application de la vidéo Palindrome (ex: 121):

gen_carre = (x**2 for x in range(1000))
palindrome = (x for x in gen_carre if str(x) == str(x)[::-1]
palindrome
<generator object ... >
# on force explicitement la création de la liste résultat
list(palindrome)
[0,
1,
...
121,
484,
...
698896]

Fonctions génératrices

Exemple de base de la vidéo:

def gen():
    yield 10
 
gen()
<function gen at 0x00...> # c'est une fonction generatrice
g = gen()
next(g) # on itere la fonction d'un pas
10
next(g) # on itere la fonction d'un second pas
StopIteration ...

2ème exemple de base de la vidéo:

def gen(x):
    yield x
    x = x +1
    yield x
 
g = gen(10)
next(g) # on itere la fonction d'un pas
10
next(g) # on itere la fonction d'un second pas
11
next(g) # on itere la fonction d'un 3ème pas
StopIteration ...

3ème exemple de base de la vidéo:

def carre(a, b):
    for i in range (a, b):
        yield  i **2
 
c = carre(1, 10)
list(c)
[1, 4, 9, ... 81]

4ème exemple de base de la vidéo: Palindrome

def palindrome(it): #it: itérateur
    for i in it:
        if (isinstance(i, (str, int)) and  str(i) == str(i)[::-1]): # instance d'une chaîne  ou d'1 entier
            yield i
 
# appel avec une liste en paramètre
p = palindrome([121, 10, 12321, 'abc', 'abba'])
list(p)
[121, 12321, 'abba']
 
# appel avec une expression génératrice en paramètre
list(palindrome(x**2 for x in range(1000))) 
[0, 1, ..., 121, 484, ..., 698896]

6. Processus d'importation des modules

Un module n'est chargé qu'une fois
Les inconvénients de ce choix - la fonction reload
python fournit dans le module importlib une fonction reload, qui permet comme son nom l'indique de forcer le rechargement d'un module.

  • importlib.reload(nomDuModule) introduite en python3.4
  • imp.reload qui est dépréciée depuis python3.4 mais qui existe toujours.

NOTE spécifique à l'environnement des notebooks (en fait, à l'utilisation de ipython) :

À l'intérieur d'un notebook, vous pouvez faire comme ceci pour recharger le code importé automatiquement : https://ipython.org/ipython-doc/3/config/extensions/autoreload.html

# charger le magic 'autoreload'
%load_ext autoreload
# activer autoreload
%autoreload 2

sys.modules

import sys
'csv' in sys.modules
False
 
import csv
'csv' in sys.modules
True
 
csv is sys.modules['csv']
True
 
del sys.modules['multiple_import']
import multiple_import

sys.builtin_module_names Signalons enfin la variable sys.builtin_module_names qui contient le nom des modules, comme par exemple le garbage collector gc, qui sont implémentés en C et font partie intégrante de l'interpréteur.

'gc' in sys.builtin_module_names
True

Où sont cherchés les modules ?

Connaître le dossier courant

from pathlib import Path
Path.cwd()

Ordre de recherche:
https://docs.python.org/fr/3/tutorial/modules.html#the-module-search-path

  • un module built-in de nom spam - possiblement/probablement écrit en C,
  • ou sinon un fichier spam.py (ou spam/init.py s'il s'agit d'un package) ; pour le localiser on utilise la variable sys.path (c'est-à-dire l'attribut path dans le module sys), qui est une liste de répertoires, et qui est initialisée avec, dans cet ordre :
  • - le répertoire où se trouve le point d'entrée ;
  • - la variable d'environnement PYTHONPATH ;
  • - un certain nombre d'emplacements définis au moment de la compilation de python.

Exemple accès Utilitaire de 3 fichiers py

/usr/share/utilitaire/
                      main.py
                      spam.py
                      eggs.py

Si vous ne faites rien de particulier, c'est-à-dire que main.py contient juste

import spam, eggs

Alors le programme ne fonctionnera que s'il est lancé depuis /usr/share/utilitaire, ce qui n'est pas du tout pratique.

Pour contourner cela on peut écrire dans main.py quelque chose comme :

# on récupère le répertoire où est installé le point d'entrée
import os.path
directory_installation = os.path.dirname(__file__)
 
# et on l'ajoute au chemin de recherche des modules
import sys
sys.path.append(directory_installation)
 
# maintenant on peut importer spam et eggs de n'importe où
import spam, eggs

Distribuer sa propre librairie avec setuptools

setuptools permet au programmeur d'écrire - dans un fichier qu'on appelle traditionnellement setup.py - le contenu de son application ; grâce à quoi on peut ensuite de manière unifiée :

  • installer l'application sur une machine à partir des sources ;
  • préparer un package de l'application ;
  • diffuser le package dans l'infrastructure PyPI ; https://pypi.python.org/pypi
  • installer le package depuis PyPI en utilisant pip3.

Pour installer setuptools, comme d'habitude vous pouvez faire simplement :

pip3 install setuptools

7. Importation des modules et espaces de nommage

Importer tout un module

import monmodule

Importer un symbole dans un module

from monmodule import monsymbole

import_module

Permet d'importer un module dont vous avez calculé le nom

modulename = "ma" + "th" 
from importlib import import_module
loaded = import_module(modulename)
type(loaded)
module

Nous avons maintenant bien chargé le module math, et on l'a rangé dans la variable loaded

# loaded référence le même objet module que si on avait fait 
# import math
import math
math is loaded
True

import as

import monmodule as autremodule
from monmodule import monsymbole as autresymbole

La notion de package

from modtools import show_module, show_package
import module_simple
Chargement du module module_simple
show_module(module_simple)
Fichier /home/jovyan/modules/module_simple.py
----------------------------------------
|print("Chargement du module", __name__)
 |
 |def spam(n):
 |    "Le polynôme (n+1)*(n-3)"
 |    return n**2 - 2*n - 3

Lorsqu'il s'agit d'implémenter une très grosse bibliothèque, il n'est pas concevable de tout concentrer en un seul fichier. C'est là qu'intervient la notion de package, qui est un peu aux répertoires ce que que le module est aux fichiers.

Le package porte le même nom que le répertoire, c'est-à-dire que, de même que le module module_jouet correspond au fichier module_jouet.py, le package python package_jouet corrrespond au répertoire package_jouet.

Pour définir un package, il faut obligatoirement créer dans le répertoire (celui, donc, que l'on veut exposer à python), un fichier nommé init.py. Voilà comment a été implémenté le package que nous venons d'importer :

show_package(package_jouet)
Fichier /home/jovyan/modules/package_jouet/__init__.py
----------------------------------------
|print("chargement du package", __name__)
 |
 |spam = ['a', 'b', 'c']
 |
 |# on peut forcer l'import de modules
 |import package_jouet.module_jouet
 |
 |# et définir des raccourcis
 |jouet = package_jouet.module_jouet.jouet

Comme on le voit, importer un package revient essentiellement à charger le fichier init.py correspondant. Le package se présente aussi comme un espace de nom, à présent on a une troisième variable spam qui est encore différente des deux autres :

package_jouet.spam
['a', 'b', 'c']

L'avantage principal du package par rapport au module est qu'il peut contenir d'autres packages ou modules. Dans notre cas, package_jouet vient avec un module qu'on peut importer comme un attribut du package, c'est-à-dire comme ceci :

import package_jouet.module_jouet

À nouveau regardons comment cela est implémenté ; le fichier correspondant au module se trouve naturellement à l'intérieur du répertoire correspondant au package, c'était le but du jeu au départ :

show_module(package_jouet.module_jouet)
Fichier /home/jovyan/modules/package_jouet/module_jouet.py
----------------------------------------
|print("Chargement du module", __name__, "dans le package 'package_jouet'")
 |
 |jouet = 'une variable définie dans package_jouet.module_jouet'

Vous remarquerez que le module module_jouet a été chargé au même moment que package_jouet. Ce comportement n'est pas implicite. C'est nous qui avons explicitement choisi d'importer le module dans le package (dans init.py).

Cette technique correpond à un usage assez fréquent, où on veut exposer directement dans l'espace de nom du package des symboles qui sont en réalité définis dans un module.

Avec le code ci-dessus, après avoir importé package_jouet, nous pouvons utiliser

package_jouet.jouet
'une variable définie dans package_jouet.module_jouet'

alors qu'en fait il faudrait écrire en toute rigueur

package_jouet.module_jouet.jouet
'une variable définie dans package_jouet.module_jouet'

Mais cela impose alors à l'utilisateur d'avoir une connaissance sur l'organisation interne de la bibliothèque, ce qui est considéré comme une mauvaise pratique.

D'abord, cela donne facilement des noms à rallonge et du coup nuit à la lisibilité, ce n'est pas pratique. Mais surtout, que se passerait-il alors si le développeur du package voulait renommer des modules à l'intérieur de la bibliothèque ? On ne veut pas que ce genre de décision ait un impact sur les utilisateurs.

À quoi sert __init__.py ?

Le code placé dans init.py est chargé d'initialiser la bibliothèque. Le fichier peut être vide mais doit absolument exister. Nous vous mettons en garde car c'est une erreur fréquente de l'oublier. Sans lui vous ne pourrez importer ni le package, ni les modules ou sous-packages qu'il contient.

C'est ce fichier qui est chargé par l'interpréteur python lorsque vous importez le package. Comme pour les modules, le fichier n'est chargé qu'une seule fois par l'interpréteur python, s'il rencontre plus tard à nouveau le même import, il l'ignore silencieusement.

Variables spéciales

__name__

print(package_jouet.__name__, package_jouet.module_jouet.__name__)
package_jouet package_jouet.module_jouet

Remarquons à cet égard que le point d'entrée du programme (c'est-à-dire, on le rappelle, le fichier qui est passé directement à l'interpréteur python) est considéré comme un module dont l'attribut name vaut la chaîne “main

C'est pourquoi (et c'est également expliqué ici https://docs.python.org/fr/3/tutorial/modules.html#executing-modules-as-scripts) les scripts python se terminent généralement par une phrase du genre de

if __name__ == "__main__":
    <faire vraiment quelque chose>
    <comme par exemple tester le module>

Cet idiome très répandu permet d'attacher du code à un module lorsqu'on le passe directement à l'interpréteur python.

__file__

print(package_jouet.__file__) 
print(package_jouet.module_jouet.__file__)
/home/jovyan/modules/package_jouet/__init__.py
/home/jovyan/modules/package_jouet/module_jouet.py

__all__

Il est possible de redéfinir dans un package la variable all, de façon à définir les symboles qui sont réellement concernés par un import *, comme c'est décrit ici. https://docs.python.org/fr/3/tutorial/modules.html#importing-from-a-package

Pour en savoir plus

Semaine 6. Conception des classes

1. Classes, instances et méthodes

Résolution dynamique le long de l'arbre d'héritage:
On ajoute un attribut à la volé à la Class:
L'instance qui en dépend, hérite de ce nouvel attribut.

vars(Class ou Inst.)

Connaître l'espace de nommage d'une Class ou d'une instance

class Bateau:
    def __init__(self, id, name, country):
        self.id = id
        self.name = name
        self.country = country
 
bateau2 = Bateau(1000, "Toccata", "FRA")
 
vars(bateau2)
{'country': 'FRA', 'id': 1000, 'name': 'Toccata'}

bound

p.initia(s)
<bound methode Phrase.initia ... >
# donc
p.initia(s)
# est équivalent à :
Phrase.initia(p, s)

Reserved classes of identifiers (Private vs Public)

self

Lors de l'écriture d'une Class :
Self et le paramètre de méthode qui référence l'instance.

2. Méthodes spéciales

Exemples donnés dans la vidéo du cours :

def __init__(self):
def __len__(self):
def __contains__(self, machin): # machin in truc
def __str__(self):

3. Héritage

Charge mémoire d'un objet:

import sys
 
# p1 = dict / p2 = instance / p3 = namedtuple
 
for p in p1, p2, p3:
    print(sys.getsizeof(p))

Énumérations

Enum

from enum import Enum
 
class Flavour(Enum):
    CHOCOLATE = 1
    VANILLA = 2
    PEAR = 3
 
vanilla = Flavour.VANILLA
str(vanilla)
'Flavour.VANILLA'
repr(vanilla)
'<Flavour.VANILLA: 2>'

Vous pouvez aussi retrouver une valeur par son nom :

chocolate = Flavour['CHOCOLATE']
chocolate
<Flavour.CHOCOLATE: 1>
Flavour.CHOCOLATE
<Flavour.CHOCOLATE: 1>

Et réciproquement :

chocolate.name
'CHOCOLATE'

IntEnum

En fait, le plus souvent on préfère utiliser IntEnum, une sous-classe de Enum qui permet également de faire des comparaisons. Pour reprendre le cas des codes d'erreur HTTP :

from enum import IntEnum
 
class HttpError(IntEnum):
 
    OK = 200
    REDIRECT = 301
    REDIRECT_TMP = 302
    NOT_FOUND = 404
    INTERNAL_ERROR = 500
 
    # avec un IntEnum on peut faire des comparaisons
    def is_redirect(self):
        return 300 <= self.value <= 399
 
code = HttpError.REDIRECT_TMP
code.is_redirect()
True

https://docs.python.org/fr/3/library/enum.html

4. Héritage multiple et ordre de résolution des attributs

MRO: Method Resolution Order

Comment est-ce que Python fait pour savoir quelle est la Class qui à créée mon instance, et quelle sont les Super Class de ma Class ?

  • attribut instance c. class qui retourne une référence vers l'objet Class qui a créée cette instance.
  • les class on un tuple C. bases qui contient toutes les super class de la class.
class C
    pass
 
c = C()
c.__class__.C
 
C.__bases__
(object,)

La méthode mro()

Elle est définie par défaut dans object:

C.mro()
[__main__.C, object]

Avec des Class imbriquées: Héritage multiple

class SuperA:
    pass
class SuperB:
    pass
class C(SuperA, SuperB):
    pass
 
C.mro()
[__main__.C, __main__.SuperA, __main__.SuperB, object]

Inversons l'ordre des super class pour contrôler la méthode mro()

class C(SuperB, SuperA):
    pass

C.mro()
[__main__.C, __main__.SuperB, __main__.SuperA, object]

Algorithme de Python:

Pour en savoir plus:

  1. Un blog de Guido Van Rossum qui retrace l'historique des différents essais qui ont été faits avant de converger sur le modèle actuel.
    http://python-history.blogspot.fr/2010/06/method-resolution-order.html
  2. Un article technique qui décrit le fonctionnement de l'algorithme de calcul de la MRO, et donne des exemples.
    https://www.python.org/download/releases/2.3/mro/
  3. L'article de wikipedia sur l'algorithme C3.
    https://en.wikipedia.org/wiki/C3_linearization

5. Variables et attributs

Les attributs: Les fonctions de gestion des attributs

Pour accéder programmativement aux attributs d'un objet, on dispose des 3 fonctions built-in getattr, setattr, et hasattr, que nous allons illustrer tout de suite.

Lire un attribut

import math
# nous savons lire un attribut comme ceci 
# qui lit l'attribut de nom 'pi' dans le module math
math.pi
3.141592653589793

La fonction built-in getattr permet de lire un attribut programmativement :

# si on part d'une chaîne qui désigne le nom de l'attribut
# la formule équivalente est alors
getattr(math, 'pi')
3.141592653589793
# on peut utiliser les attributs avec la plupart des objets
# ici nous allons le faire sur une fonction
def foo(): 
    "une fonction vide"
    pass
 
# on a déjà vu certains attributs des fonctions
print(f"nom={foo.__name__}, docstring=`{foo.__doc__}`")
nom=foo, docstring=`une fonction vide`
# on peut préciser une valeur par défaut pour le cas où l'attribut
# n'existe pas
getattr(foo, "attribut_inexistant", 'valeur_par_defaut')
'valeur_par_defaut'

Écrire un attribut

# on peut ajouter un attribut arbitraire (toujours sur l'objet fonction)
foo.hauteur = 100
 
foo.hauteur
100

Comme pour la lecture on peut écrire un attribut programmativement avec la fonction built-in setattr :

# écrire un attribut avec setattr
setattr(foo, "largeur", 200)
 
# on peut bien sûr le lire indifféremment
# directement comme ici, ou avec getattr
foo.largeur
200

Liste des attributs

La fonction built-in hasattr permet de savoir si un objet possède ou pas un attribut :

# pour savoir si un attribut existe
hasattr(math, 'pi')
True

Ce qui peut aussi être retrouvé autrement, avec la fonction built-in vars :

vars(foo)
{'hauteur': 100, 'largeur': 200}

Sur quels objets

Il n'est pas possible d'ajouter des attributs sur les types de base, car ce sont des classes immuables :

for builtin_type in (int, str, float, complex, tuple, dict, set, frozenset):
    obj = builtin_type()
    try: 
        obj.foo = 'bar'
    except AttributeError as e: 
        print(f"{builtin_type.__name__:>10} → exception {type(e)} - {e}")
 
 int → exception <class 'AttributeError'> - 'int' object has no attribute 'foo'
       str → exception <class 'AttributeError'> - 'str' object has no attribute 'foo'
     float → exception <class 'AttributeError'> - 'float' object has no attribute 'foo'
   complex → exception <class 'AttributeError'> - 'complex' object has no attribute 'foo'
     tuple → exception <class 'AttributeError'> - 'tuple' object has no attribute 'foo'
      dict → exception <class 'AttributeError'> - 'dict' object has no attribute 'foo'
       set → exception <class 'AttributeError'> - 'set' object has no attribute 'foo'
 frozenset → exception <class 'AttributeError'> - 'frozenset' object has no attribute 'foo'

C'est par contre possible sur virtuellement tout le reste, et notamment là où c'est très utile, c'est-à-dire pour ce qui nous concerne sur les :

  • modules
  • packages
  • fonctions
  • classes
  • instances

Espaces de nommage

Attributs

Un attribut est un symbole x utilisé dans la notation obj.x où obj est l'objet qui définit l'espace de nommage sur lequel x existe.

L'affectation (explicite ou implicite) d'un attribut x sur un objet obj va créer (ou altérer) un symbole x directement dans l'espace de nommage de obj, symbole qui va référencer l'objet affecté, typiquement l'objet à droite du signe =

lass MaClasse:
    pass
 
# affectation explicite
MaClasse.x = 10 
 
# le symbole x est défini dans l'espace de nommage de MaClasse
'x' in MaClasse.__dict__
True

Le référencement (la lecture) d'un attribut va chercher cet attribut le long de l'arbre d'héritage en commençant par l'instance, puis la classe qui a créé l'instance, puis les super-classes et suivant la MRO (voir le complément sur l'héritage multiple).

Variables

Une variable est un symbole qui n'est pas précédé de la notation obj. et l'affectation d'une variable rend cette variable locale au bloc de code dans lequel elle est définie, un bloc de code pouvant être :

  • une fonction, dans ce cas la variable est locale à la fonction ;
  • une classe, dans ce cas la variable est locale à la classe ;
  • un module, dans ce cas la variable est locale au module, on dit également que la variable est globale.

Une variable référencée est toujours cherchée suivant la règle LEGB :

  • localement au bloc de code dans lequel elle est référencée ;
  • puis dans les blocs de code des fonctions ou méthodes englobantes, s'il y en a, de la plus proche à la plus eloignée ;
  • puis dans le bloc de code du module.

Si la variable n'est toujours pas trouvée, elle est cherchée dans le module builtins et si elle n'est toujours pas trouvée, une exception est levée.
Par exemple :

var = 'dans le module'
 
class A:
    var = 'dans la classe A'
    def f(self):
        var = 'dans la fonction f'
        class B:
            print(var)
        B()
A().f()

dans la fonction f

En résumé

Dans la vidéo et dans ce complément basique, on a couvert tous les cas standards, et même si python est un langage plutôt mieux fait, avec moins de cas particuliers que d'autres langages, il a également ses cas étranges entre raisons historiques et bugs qui ne seront jamais corrigés (parce que ça casserait plus de choses que ça n'en réparerait). Pour éviter de tomber dans ces cas spéciaux, c'est simple, vous n'avez qu'à suivre ces règles :

  • ne jamais affecter dans un bloc de code local une variable de même nom qu'une variable globale ;
  • éviter d'utiliser les directives global et nonlocal, et les réserver pour du code avancé comme les décorateurs et les métaclasses ;
  • et lorsque vous devez vraiment les utiliser, toujours mettre les directives global et nonlocal comme premières instructions du bloc de code où elle s'appliquent.

Si vous ne suivez pas ces règles, vous risquez de tomber dans un cas particulier que nous détaillons ci-dessous dans la partie avancée.

7. Conception d'exceptions personnalisées [à faire]

8. Conception de context manager

Exemple d'utilisation des context manager du site python :

>>> with open('workfile', 'r') as f:
...     read_data = f.read()
>>> f.closed
True

Exemple de la vidéo

import time
class Timer:
    def __enter__(self):
        self.start = time.time()
        return self
 
    def __exit__(self, *args):
        duree = time.time() - self.start
        print(f"{duree}s")
        return False # en cas d'exeption, elle est remontée et arrête le programme
                     # si True, ce prog capture l'exception et doit la traiter
 
    def __str__(self):
        duree = time.time() - self.start
        return f"intermédiaire: {duree}s"
 
with Timer() as t:
    sum(x for x in range(10_000_000))
    print(t)
    1/0
    sum(x**2 for x in range(10_000_000))
 
intermédiaire: 0.73420...
07498..
Traceback ...
ZeroDivisionError: division by zero

Les paramètres de __exit__

la méthode exit reçoit trois arguments : https://docs.python.org/fr/3/reference/datamodel.html#with-statement-context-managers

def __exit__(self, exc_type, exc_value, traceback):

Lorsqu'on sort du bloc with sans qu'une exception soit levée, ces trois arguments valent None. Par contre si une exception est levée, ils permettent d'accéder au type, à la valeur de l'exception, et à l'état de la pile lorsque l'exception est levée.