Formation permanente du CNRS, Délégation Alsace
Février - Mars 2017
Auteurs :
Contenu sous licence CC BY-SA 4.0
%edit ../J2/exos/meteo_json.py
"""
Process a weather forecast json file to plot the time evolution of today's
temperature in Strasbourg
"""
import urllib.request, urllib.error, urllib.parse
import json
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import numpy as np
jsonfile_url = "http://www.prevision-meteo.ch/services/json/Strasbourg"
f = urllib.request.urlopen(jsonfile_url) # open url
json = json.loads(f.read().decode('utf8')) # Read JSON file
day = json['fcst_day_0'] # point the current day data
day_hd = day['hourly_data'] # point to hourly data
# Get tempe = [[h1, T1], [h2, T2], ...] list
# where h1 is the time in hour and T2 is the temperature in deg. C
tempe = []
for hour, data in day_hd.items():
# get first part of time in "00H00" format and remove "H00"
# get temperature at 2m above ground 'TMP2m'
tempe.append([int(hour[:-3]), data['TMP2m']])
# Alternative form using list comprehension:
# tempe = [[int(hour[:-3]), data['TMP2m']] for hour, data in day_hd.iteritems()]
tempe.sort() # Sort temperatures according to the hour of day
t = np.array(tempe).transpose() # Transpose list of (hour, tempe)
# Plot T = T(hour)
fig = plt.figure() # initialise figure
title = "{} {}".format(day['day_long'], day['date'])
fig.suptitle(title, fontsize=14, fontweight='bold')
ax = fig.add_subplot(111) # initialise a plot area
fig.subplots_adjust(top=0.85)
ax.set_title('Day temperature')
ax.set_xlabel('Time [h]')
ax.set_ylabel('Temperature [deg. C]')
ax.plot(t[0], t[1]) # plot t[1] (tempe) as a function of t[0] (hour)
# Add meteo icon to plot
icon = urllib.request.urlopen(day['icon_big']) # Open weather icon
axicon = fig.add_axes([0.8, 0.8, 0.15, 0.15])
img = mpimg.imread(icon) # initialise image
axicon.set_xticks([]) # Remove axes ticks
axicon.set_yticks([])
axicon.imshow(img) # trigger the image show
Si on veut le réutiliser, il faut le structurer en fonctions que l'on peut appeler depuis un programme principal.
%edit ../J2/exos/meteo_json_func.py
#!/usr/bin/env python3
"""
Process a weather forecast json file to plot the time evolution of temperature
of a given day in a given city
"""
import urllib.request
import json
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import numpy as np
def get_city_from_user():
"""From user input, return a meteo json dictionary corresponding to city"""
while True: # Infinite loop to handle city name input
city_name = input("Entrer la ville :\n")
jsonfile_url = "http://www.prevision-meteo.ch/services/json/"\
+ city_name
f = urllib.request.urlopen(jsonfile_url) # open url
city_json = json.loads(f.read().decode('utf8'))
if 'errors' in city_json:
print("{} n'existe pas dans la base. Essayez un autre nom."
.format(city_name))
else:
return city_json
def plot_day_tempe(city_json, day_key):
"""Plot Temperature vs hour from a json dictionary for a given day_key"""
city_name = city_json['city_info']['name']
day = city_json[day_key]
day_hd = day['hourly_data'] # point to hourly data
# Get tempe = [[h1, T1], [h2, T2], ...] list
# where h1 is the time in hour and T2 is the temperature in deg. C
tempe = []
for hour, data in day_hd.items():
# get first part of time in "00H00" format and remove "H00"
# get temperature at 2m above ground 'TMP2m'
tempe.append([int(hour[:-3]), data['TMP2m']])
# Alternative form using list comprehension:
# tempe = [[int(hour[:-3]), data['TMP2m']] for hour, data in day_hd.iteritems()]
tempe.sort() # Sort temperatures according to the hour of day
t = np.array(tempe).transpose() # Transpose list of (hour, tempe)
# Plot T = T(hour)
fig = plt.figure() # initialise figure
title = "{} {} à {}".format(day['day_long'], day['date'], city_name)
fig.suptitle(title, fontsize=14, fontweight='bold')
ax = fig.add_subplot(111) # initialise a plot area
fig.subplots_adjust(top=0.85)
ax.set_title('Evolution horaire')
ax.set_xlabel('Temps [h]')
ax.set_ylabel('Température [deg. C]')
ax.plot(t[0], t[1]) # plot t[1] (tempe) as a function of t[0] (hour)
# Add meteo icon to plot
icon = urllib.request.urlopen(day['icon_big']) # Open weather icon
axicon = fig.add_axes([0.8, 0.8, 0.15, 0.15])
img = mpimg.imread(icon) # initialise image
axicon.set_xticks([]) # Remove axes ticks
axicon.set_yticks([])
axicon.imshow(img) # trigger the image show
plt.show() # trigger the figure show
def get_day(city_json):
"""From user input, return the day key in json dictonary"""
days = {day: data['day_long'] for day, data in city_json.items()
if day[:8] == "fcst_day"} # Create {'fcst_day_#': week-day}
question = "Choisir le jour :\n"
days_list = [] # Build ['fcst_day_#', week-day] sorted list
# This i-loop is required because "days" is not sorted:
for i in range(5):
key = "fcst_day_{}".format(i)
days_list.append([key, days[key]])
question = question + "{}) {}\n".format(i, days[key])
while True:
try:
choice = int(input(question)) # Prompt user for day index
return days_list[choice][0] # Return 'fcst_day_#'
except:
print("Entrée non valide.")
if __name__ == '__main__':
# This block is not executed if this file is imported as a module
city_json = get_city_from_user() # get json dict from user input
day_key = get_day(city_json) # get day key from user input
plot_day_tempe(city_json, day_key) # plot day temperature evolution
Si on définit une nouvelle fonction pour récupérer le dictionnaire de la ville :
import sys
def get_city(city_name):
"""return a meteo json dictionary corresponding to city_name"""
jsonfile_url = "http://www.prevision-meteo.ch/services/json/"\
+ city_name
f = urllib.request.urlopen(jsonfile_url) # open url
city_json = json.load(f)
if 'errors' in city_json:
msg = "La ville n'est pas dans la base"
sys.exit(msg)
else:
return city_json
On peut maintenant importer ses fonctions séparément :
city_json = get_city("Toulouse")
plot_day_tempe(city_json, 'fcst_day_1')
La programmation orientée objet est un paradigme de programmation qui introduit en particulier les concepts suivants :
L'encapsulation est la manière d'associer les données et les méthodes qui s'y appliquent dans un même type appelé objet.
Les objets sont les instances de ces classes. Ils sont créés en appelant la classe comme une fonction :
a = MaClasse(45, True, 'toto')
b = MaClasse(78, False, 'titi')
L'objet a
est une instance de la classe MaClasse
.
b
est une autre instance de MaClasse
.
class Vecteur():
commentaire = "Cette classe est top"
def __init__(self, x, y, z):
self.coord = [x, y, z]
v1 = Vecteur(1., 2., 3.)
v2 = Vecteur(0., 0., 0.)
v1
et v2
sont deux instances de la classe Vecteur
self
:self.un_attribut
__init__()
commentaire
est un attribut de la classe qui ne dépend pas de son instanciationprint(Vecteur.commentaire)
self.coord
est un attribut qui est construit lors de l'instanciationprint(v1.coord, v2.coord)
print('commentaire' in dir(Vecteur)) # dir() renvoie la liste des attributs de l'objet
print('commentaire' in dir(v1))
print('coord' in dir(Vecteur))
print('coord' in dir(v1))
La surcharge d'opérateur consiste à redéfinir des fonctions existantes de la classe.
Ici, afin de représenter le contenu de l'objet, on surcharge la méthode __repr__()
.
class Vecteur():
commentaire = "Cette classe est top"
def __init__(self, x, y, z):
self.coord = [x, y, z]
def __repr__(self):
"""On surcharge l'opérateur __repr__ en renvoyant
une chaîne de caractère"""
s = ''
for c in self.coord:
s += '({})\n'.format(c)
return s
La fonction print()
appliquée à l'instance fait un appel à __repr__()
.
v1 = Vecteur(1., 2., 3.)
print(v1)
Un autre exemple de surcharge d'une fonction intrinsèque : __add__()
def __add__(self, v):
return Vecteur(self.coord[0] + v.coord[0],
self.coord[1] + v.coord[1],
self.coord[2] + v.coord[2])
Vecteur.__add__ = __add__
v1 = Vecteur(1., 2., 3.)
v2 = Vecteur(3., 2., 1.)
print(v1 + v2)
Exercice : Implémenter la méthode scal()
qui renvoie le produit scalaire de deux vecteurs $(\vec{v}\cdot \vec{w} = v_{x}w_{x}+v_{y}w_{y}+v_{z}w_{z})$.
def scal(self, v):
# --- votre code ici ---
pass
Vecteur.scal = scal
v1 = Vecteur(2., 3., 4.)
v2 = Vecteur(6., 4., 3.)
print(v1.scal(v2))
Correction dans J3/exos/vecteur.py
Pour spécifier les classes dont on hérite, il suffit de les lister en paramètres de la définition de classe :
class Fille(Parent1, Parent2):
pass
Note : Nous n'aborderons pas les classes ayant plusieurs ancêtres dans le cadre de ce cours. Vous trouverez la documentation sur ce sujet ici.
En guise d'exemple, prenons la classe ci-dessous.
class Humain():
def __init__(self, nom, age):
self.nom = nom
self.age = age
def vieux(self):
return self.age >= 50
Créons maintenant des instances de cette classe.
a = Humain('Alain', 42)
b = Humain('Bertrand', 53)
c = Humain('Corine', 37)
fratrie = [a, b ,c]
for h in fratrie:
print('{} est {}'.format(h.nom, 'vieux' if h.vieux() else 'jeune' ))
Illustrons maintenant le mécanisme d'héritage.
class Personne(Humain):
def __init__(self, nom, age, sexe):
super(Personne, self).__init__(nom, age)
self.sexe = sexe
def porte_les_bebes(self):
return self.sexe.lower() in ('f', 'femme', 'fille')
a = Personne('Alain', 42, 'h')
b = Personne('Bertrand', 53, 'h')
c = Personne('Corinne', 37, 'f')
pfratrie = [a, b ,c]
Humain
à la classe Personne
Personne
hérite des méthodes de la Humain
for p in pfratrie:
print('{} est {}'.format(p.nom, 'vieux' if p.vieux() else 'jeune' ))
__init__()
, nous surchargeons la classe Humain
.super()
permet d'appeler le constructeur de la classe mère.porte_les_bebes()
, nous étendons la classe Humain
.for p in pfratrie:
if p.porte_les_bebes():
print('{} peut porter des bébés'.format(p.nom))
On veut superposer les courbes heure-température de plusieurs villes sur le même graphe. Pour ce faire, il faut :
City
qui dispose d'une méthode qui retourne les tableaux 1D hour et temperature pour un jour donné.Pour vous simplifier le travail, nous avons écrit l'ébauche d'un script python dans exos/meteo.py. En l'état, ce script s'exécute mais ne fait rien. Editez-le dans Spyder et écriver le code nécessaire pour le rendre fonctionnel là où les commentaires vous y invitent.
Lorsque votre implantation sera terminée, la cellule ci-dessous devra s'exécuter correctement.
Note : Pour importer le module
meteo
depuis le packageexos
, il faut que le répertoireexos
contiennent un fichier__init__.py
from exos import meteo
toulouse = meteo.City('Toulouse')
paris = meteo.City('Paris')
meteo.plot_day_temperature(toulouse, paris, day_number=3)
argparse
¶Dans un nouveau fichier python :
J3/exos/correction/meteo_city.py
comme un moduleargparse
(cf. Gestion des arguments)La solution est ici.
Toujours dans l'objectif de superposer des courbes de températures, on veut pouvoir charger également des données définies par leurs coordonnées géographiques.
En effet, le site www.prevision-meteo.ch supporte les requêtes sous la forme
http://www.prevision-meteo.ch/services/json/lat=45.32lng=10
où lat=45.32lng=10
désigne la latitude et la longitude.
Pour ce faire, à partir de la classe City
, on va dériver la classe Location
qui sera instanciée de la façon suivante :
trou_perdu = Location(lat=45.32, lng=10)
À partir du fichier exos/correction/meteo_city.py, écrivez la classe dérivée Location
.
Lorsque votre implantation sera terminée, la cellule ci-dessous devra s'exécuter correctement.
from exos import meteo
toulouse = meteo.City('Toulouse')
paris = meteo.City('Paris')
# --- Décommentez ci-dessous ---
# trou_perdu = meteo.Location(lat=45.32, lng=10)
# meteo.plot_day_temperature(toulouse, paris, trou_perdu, day_number=3)
Continuez a augmenter l'ergonomie de votre script en ajoutant/modifiant une ou plusieurs options en ligne de commande pour gérer les locations, les villes, etc...
Elles permettent de définir très concisement des fonctions anonymes (sans nom, comme avec def
).
# Création d'une fonction et assignation a une variable
func = lambda [arg1]...: expr
# Appel de fonction par la variable
func(...)
ce qui est équivalent à:
def _noname([arg1]...):
return expr
func = _noname
del _noname
func(...)
En plus court...
Plus d'informations sur les expressions lambda
.
Elles sont utilisées par exemple, pour passer des petits bouts de code en paramètre à d'autres fonctions :
l1 = list('AlpHabetIZation')
l2 = list(l1)
l1.sort(key=lambda x: x.lower())
l2.sort()
print(l1)
print(l2)
Python permet d'écrire assez facilement des tests unitaires, grâce au module unittest
de sa librairie standard. Ce module ressemble assez aux tests unitaires java JUnit.
vieux(age)
qui retourne une valeur booléenne indiquant si l'age donné est au-dessus d'un seuil (une constante du module)vieux.vieux()
%run exos/tunitaires.py
doctest
.En pratique dans des projets plus importants, les tests sont agencés en une suite de tests. L'exécution des suites de tests est effectuée grâce a des pilotes comme py.test ou nose. Ces pilotes permettent, entre autres, le lancement de multiples tests en parallèle ainsi que des rapports de couverture de code.
Pour installer des modules additionnels disponibles par exemple sur le Python Package Index, vous pouvez utiliser le gestionnaire de paquet pip
pip install valerius
Cette commande fera une installation système (il vous faut donc être administrateur de la machine) mais vous pouvez faire une installation dans votre compte avec l'option --user
. Il faudra alors ajouter le chemin ~/.local/lib/python3.5/site-packages
à la variable d'environnement PYTHONPATH
pour que python trouve les paquets isntallés au moment d'un import
.
Pour fabriquer des paquets installables de cette manière, visitez cette page.
Pour pouvoir profiter de plusieurs environnements python qui ont des modules différents installés, il y a le mécanisme venv.
$ mkdir test_py35
$ virtualenv -p python3.5 test_py35
$ test_py35/bin/activate
(test_py35) $
Vous êtes alors dans un environnement où vous pouvez installer des paquets indépendemment du système :
(test_py35) $ pip install valerius
help()
.docstring
pour d'autres usages, comme la génération automatique de pages de documentation HTML ou de contenir le code de tests unitaires (c.f. doctest
).class LaClasse():
'''La classe, c'est la classe'''
pass
def func():
'''La fonction func() nous met le funk !'''
pass
help(LaClasse)
help(func)