Résumé PEP8 par sam&max :
http://sametmax.com/le-pep8-en-resume/
PEP8:
http://legacy.python.org/dev/peps/pep-0008/#descriptive-naming-styles
https://docs.python.org/3/reference/lexical_analysis.html#keywords
>>> x = 1 >>>type(x) int >>> isinstance(23, int) True
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
48 / 5 division naturelle // quotient % modulo ** puissance
Entier int Flottant float Complexe complex Chaîne str
deux_cents = 0o310 # binaire deux_cents = 0o310 # octale deux_cents = 0xc8 # hexa deux_cents = int('3020', 4) # base 4
# 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
from decimal import Decimal Decimal('0.3') - Decimal('0.1') == Decimal('0.2') -> True
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
# -*- coding: <nom_de_l_encodage> -*- ou # coding: utf-8 #à utiliser de préférence
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/
Les outils de base sur les strings
>>> help(str)
>>> '123'.isdecimal() True
>>> '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;'
>>> "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'
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']
>>> # 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
>>> "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'
>>> 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)
>>> n = 'Thomas' >>> age = 16 >>> f"{n} a {age} ans" 'Thomas a 16 ans'
>>> # 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'
>>> 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'
>>> 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.
# 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 €
print(10*"=") ==========
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
Documentation: https://docs.python.org/3/library/re.html
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”
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', '']
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.
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)}")
>>> # 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
# 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 :
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'
Slice sans pas
>>> chaine = "abcdefghijklmnopqrstuvwxyz" ; print(chaine) abcdefghijklmnopqrstuvwxyz >>> chaine[2:6] 'cdef'
Conventions de début et fin
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]
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]])
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
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/)
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
# 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
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)])
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
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)]
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
Une clé doit être globalement immuable
Type | Mutable ? |
---|---|
int, float | immuable |
complex,bool | immuable |
str | immuable |
list | mutable |
dict | mutable |
set | mutable |
frozenset | immuable |
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
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}
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'
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 :
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
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 |
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.
Pour voir un module:
from modtools import show_module show_module(nom_du_module)
Dans le cas général, il est possible de combiner les 4 formes d'arguments:
É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"
# 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'}
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)
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
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
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
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)]
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
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']
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'}
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}
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
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]]
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)]]
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)]]
C'est comme une compréhension de liste :
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]
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 ...
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 ...
def carre(a, b): for i in range (a, b): yield i **2 c = carre(1, 10) list(c) [1, 4, 9, ... 81]
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]
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.
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
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
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
/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
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 :
Pour installer setuptools, comme d'habitude vous pouvez faire simplement :
pip3 install setuptools
Importer tout un module
import monmodule
Importer un symbole dans un module
from monmodule import monsymbole
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 monmodule as autremodule
from monmodule import monsymbole as autresymbole
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.
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.
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.
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
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
https://docs.python.org/fr/3/tutorial/modules.html
https://docs.python.org/fr/3/tutorial/modules.html#packages
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.
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'}
p.initia(s) <bound methode Phrase.initia ... > # donc p.initia(s) # est équivalent à : Phrase.initia(p, s)
https://docs.python.org/fr/3/reference/lexical_analysis.html#reserved-classes-of-identifiers
https://docs.python.org/fr/3/tutorial/classes.html#tut-private
Lors de l'écriture d'une Class :
Self et le paramètre de méthode qui référence l'instance.
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):
Charge mémoire d'un objet:
import sys # p1 = dict / p2 = instance / p3 = namedtuple for p in p1, p2, p3: print(sys.getsizeof(p))
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'
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
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 ?
class C pass c = C() c.__class__.C C.__bases__ (object,)
Elle est définie par défaut dans object:
C.mro() [__main__.C, object]
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]
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.
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'
# 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
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}
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 :
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).
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 variable référencée est toujours cherchée suivant la règle LEGB :
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
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 :
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.
Exemple d'utilisation des context manager du site python :
>>> with open('workfile', 'r') as f: ... read_data = f.read() >>> f.closed True
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
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.