Initiation à la programmation avec Ruby, Python et la NASA : 4ème partie

Avant-dernier chapitre de notre initiation à la programmation avec Ruby et Python.

De retour à Python, nous allons expérimenter de nouvelles fonctions pour notre app à la recherche d'exoplanètes.

Nous en profiterons également pour faire un peu de ‘refactoring’ : réorganiser le code pour rendre les modules de notre script un peu plus logiques.

Cet article est le quatrième d'une série de cinq.

Update 2014-08-24 : le serveur exoapi.com utilisé dans ce tutoriel est souvent en panne…

The Full Monty

Voici notre précédent script Ruby traduit en Python.

Il y a surtout des différences cosmétiques…

exo3b.py

# _*_ encoding: utf-8 _*_
import sys
import json
import urllib

class NasaExo():

    def __init__(self, params):
        self.year = params[1]
        self.api_base = 'http://exoapi.com/api/skyhook/'

    def get_planets(self):
        return json.loads(self.download(self.make_url_planets_year()))['response']['results']

    def download(self, url):
        return urllib.urlopen(url).read()

    def get_names(self, planet_list):
        return [planet['name'] for planet in planet_list]

    def print_list(self, my_list):
        for obj in my_list:
            print obj

    def make_url_planets_year(self):
        return self.api_base + 'planets/search?disc_year=' + self.year

    def print_names(self):
        return self.print_list(self.get_names(self.get_planets()))

    def make_details(self):
        result = []
        for obj in self.get_planets():
            x = {
                'name': obj['name'],
                'class': obj['mass_class'],
                'atmosphere': obj['atmosphere_class'],
                'composition': obj['composition_class'],
                'mass': obj['mass'],
                'gravity': obj['gravity'],
                'size': obj['appar_size'],
                'star': obj['star']['name'],
                'constellation': obj['star']['constellation']
            }
            result.append(x)
        return result

    def print_details(self):
        for planet_details in self.make_details():
            print "Name".ljust(16), ":", planet_details['name']
            del planet_details['name']
            for key,value in planet_details.iteritems():
                print key.capitalize().ljust(16), ":", str(value).capitalize()
            print ""

exo = NasaExo(sys.argv)
exo.print_details()

Une nouveauté cependant, dans la méthode get_names :

return [planet['name'] for planet in planet_list]

On appelle ça une “comprehension list” et c'est la même fonctionnalité que .map en Ruby.

Ici, on crée une liste contenant chaque élément planet['name'] de chaque planet dans planet_list, puis on la retourne au demandeur.

Autre chose: la méthode print_details est sensiblement différente.

Les raisons sont complexes et il est trop tôt pour les aborder, ça compliquerait trop ce tutoriel (indice : les dictionnaires ne sont pas ordonnés en Python).

Nouveaux concepts

Voyons maintenant les “conditions”.

Par exemple, vous voulez afficher une planète de la liste uniquement si sa taille est inférieure à une valeur de votre choix.

Nous allons utiliser pour cela if.

Mais avant de l'utiliser dans notre app, un petit exemple tout simple :

exo4a.py

import sys
mot = sys.argv[1]
longueur = len(mot)
if longueur < 5:
    print "Mot court:",
else:
    print "Mot long:",
print longueur, "lettres"
> python exo4a.py bonjour

Résultat:

Mot long: 7 lettres

Simplissime: “si” condition remplie alors option 1, “sinon” option 2.

Un autre exemple :

exo4b.py

# _*_ encoding: utf-8 _*_
import sys
mot = sys.argv[1].lower()
if mot == "merci":
    print "Bravo! C'était le mot magique."
elif mot == "wtf":
    print "En effet, ce 'elif' est un peu WTF. Je préfèrerais 'elsif' ou 'else if'..."
else:
    print "Vous avez tapé", mot.upper(), "et puis c'est tout."
> python exo4b.py bonjour
> python exo4b.py wtf
> python exo4b.py merci

Vous avez remarqué le “double égal”, == ?

En Python comme en Ruby et comme dans la plupart des langages, on attribue avec = et on compare avec ==.

Ben voilà, vous en savez à peu près assez pour passer à la suite du développement de notre app. :)

Go !

Nous disions donc : vous voulez afficher une planète de la liste uniquement si sa taille est inférieure à une valeur de votre choix.

En astronomie, une masse de 1 représente la même masse que la Terre : une masse de 2 représente le double, 3 le triple, etc.

Disons qu'on va chercher les planètes inférieures à une masse de 200 (il n'y en a pas beaucoup).

Nous allons partir du résultat donné par notre méthode make_details et faire un tri (dans une nouvelle méthode print_little_ones):

exo4c.py

# _*_ encoding: utf-8 _*_
import sys
import json
import urllib

class NasaExo():

    def __init__(self, params):
        self.year = params[1]
        self.api_base = 'http://exoapi.com/api/skyhook/'

    def get_planets(self):
        return json.loads(self.download(self.make_url_planets_year()))['response']['results']

    def download(self, url):
        return urllib.urlopen(url).read()

    def get_names(self, planet_list):
        return [planet['name'] for planet in planet_list]

    def print_list(self, my_list):
        for obj in my_list:
            print obj

    def make_url_planets_year(self):
        return self.api_base + 'planets/search?disc_year=' + self.year

    def print_names(self):
        return self.print_list(self.get_names(self.get_planets()))

    def make_details(self):
        result = []
        for obj in self.get_planets():
            x = {
                'name': obj['name'],
                'class': obj['mass_class'],
                'atmosphere': obj['atmosphere_class'],
                'composition': obj['composition_class'],
                'mass': obj['mass'],
                'gravity': obj['gravity'],
                'size': obj['appar_size'],
                'star': obj['star']['name'],
                'constellation': obj['star']['constellation']
            }
            result.append(x)
        return result

    def print_details(self, details):
        for planet_details in details:
            print "Name".ljust(16), ":", planet_details['name']
            del planet_details['name']
            for key,value in planet_details.iteritems():
                print key.capitalize().ljust(16), ":", str(value).capitalize()
            print ""

    def print_little_ones(self, planets, max_mass):
        littles = []
        for planet in planets:
            if planet['mass'] < max_mass:
                littles.append(planet)
        self.print_details(littles)
        print len(littles), "planets have a mass <", max_mass


exo = NasaExo(sys.argv)
planets = exo.make_details()
exo.print_little_ones(planets, 200)

Donc ici que se passe-t-il ?

On récupère la liste des planètes avec

planets = exo.make_details()

puis on appelle notre nouvelle méthode print_little_ones avec deux arguments : cette liste de planètes, et la valeur pour notre filtre.

La méthode récupère la liste et la valeur et les stocke dans deux variables, planets et max_mass :

def print_little_ones(self, planets, max_mass):

Ensuite nous créons une liste vide.

Puis nous itérons sur la liste planets, en déclarant chacun des objets de cette liste comme étant nommé planet.

Vient notre condition :

if planet['mass'] < max_mass:

On récupère la valeur donnée par le serveur pour la masse de la planète avec planet['mass'] puis on demande si cette valeur est inférieure (<) à la valeur que l'on a fourni à la méthode (max_mass).

Si la condition se vérifie (on dit: si le résultat est “True”), alors l'instruction imbriquée pour ajouter cette planète à notre nouvelle liste sera exécutée : sinon, Python passe à la suite (la prochaine planète).

On appelle alors notre méthode pré-existante print_details en lui passant notre nouvelle liste, et on conclut pour le plaisir avec un décompte du nombre de planètes correspondant à notre requête.

Nous allons maintenant ajouter quelques éléments et modifier la structure.

Classes

Notre script commence à prendre de l'ampleur, et je trouve que notre classe NasaExo est désordonnée.

Elle contient du code qui ne concerne que l'affichage de listes : ce code n'a rien à faire dans une classse dédiée à la Nasa. Pareil avec les parties concernant le réseau.

Nous allons déplacer ces méthodes dans des classes “ExoDisplay” et “ExoNetwork” et rationaliser notre script dans un souci de modularité :

exo4d.py

# _*_ encoding: utf-8 _*_
import sys
import json
import urllib

class ExoNetwork():

    def __init__(self):
        self.api_base = 'http://exoapi.com/api/skyhook/'

    def make_url_by_year(self, year):
        return self.api_base + 'planets/search?disc_year=' + year

    def download(self, url):
        return urllib.urlopen(url).read()

    def decode_json(self, response):
        return json.loads(response)

    def download_and_decode(self, url):
        return self.decode_json(self.download(url))['response']['results']

    def download_planets_by_year(self, year):
        return self.download_and_decode(self.make_url_by_year(year))

class ExoDisplay():

    def print_list(self, my_list):
        for obj in my_list:
            print obj

    def print_details(self, details):
        for planet_details in details:
            print "Name".ljust(16), ":", planet_details['name']
            del planet_details['name']
            for key,value in planet_details.iteritems():
                print key.capitalize().ljust(16), ":", str(value).capitalize()
            print ""

    def print_little_ones(self, planets, max_mass):
        littles = []
        for planet in planets:
            if planet['mass'] < max_mass:
                littles.append(planet)
        self.print_details(littles)
        print len(littles), "planets found in the database for this request"

class ExoPlanets():

    def __init__(self):
        self.network = ExoNetwork()
        self.display = ExoDisplay()

    def get_planets_by_year(self, year):
        result = []
        for obj in self.network.download_planets_by_year(year):
            x = {
                'name': obj['name'],
                'class': obj['mass_class'],
                'atmosphere': obj['atmosphere_class'],
                'composition': obj['composition_class'],
                'mass': obj['mass'],
                'gravity': obj['gravity'],
                'size': obj['appar_size'],
                'star': obj['star']['name'],
                'constellation': obj['star']['constellation']
            }
            result.append(x)
        return result

    def print_names(self, planet_list):
        print "\n- All planet names:\n"
        self.display.print_list([planet['name'] for planet in planet_list])

    def print_little_ones(self, planets, max_mass):
        print "\n- Planets with a mass less than", str(max_mass) + ":\n"
        self.display.print_little_ones(planets, max_mass)

exo = ExoPlanets()
year = sys.argv[1]
planets = exo.get_planets_by_year(year)
exo.print_names(planets)
exo.print_little_ones(planets, 200)

Woah, tout a changé !

En fait non, nous avons simplement organisé la logique de notre app de manière plus… logique.

Non seulement ça permet d'avoir un code plus aisément maintenable et modifiable, mais ça permet aussi de ne pas se répéter : en créant des objets, des petits modules, qui interagissent entre eux.

Je ne fais pas d'analyse étape par étape ici : il n'y a pas de nouveau concept ni de nouvelle syntaxe.

C'est simplement l'architecture du script qui a changé.

Next

Au prochain et dernier chapitre, nous repasserons en Ruby et étudierons des concepts un peu plus avancés.

Enjoy !

Note : les sources de ce tutoriel (fichiers Markdown, Ruby et Python) sont disponibles sur GitHub.

Auteur: Eric Dejonckheere