Post on 21-Aug-2015
Un serveur métier en Python / C++Meet-up C++ 02/10/2014Pierre Marquis & Alexandre Bonnasseau
Plan
• Retour d’expérience
• En pratique, quelques lignes de code
L'équipe dev carto
Info trafic
Itinéraire
Plan
Géolocalisation
Moteur de suggestion
Moteur de recherche géolocalisée
Itinéraire transports en commun
Python WTF ??!
Python WTF ??!
- Simplicité de code
- Framework web léger
- Richesse des modules
- Facilité d'industrialisation
Notre philosophie
- Python quand on peut
- C++ quand c’est nécessaire
Chemin de l’équipe
● Fonctions plus atomiques● Test U● Lint● Dojo● Pair programming● Code review
Schéma d’archi du plan
Chaîne d’industrialisation
Aujourd’hui
+ 80% des serveurs migrés
+ Langage très intéressant
+ Framework mieux maîtrisé
+ Dev plus rapide
+ Plus d’agilité
- Moins de maîtrise C++
- Multithread
- Débogage binding
En pratique :appeler du C++ depuis Python
Cadre
– Existence d’une base de code C++ fiable et testée
– On souhaite prototyper rapidement (besoin mouvant)
– Besoin d’exécution rapide pour des fonctions critiques
L'existant#define _USE_MATH_DEFINES
#include <cmath>
#include "geo.hpp"
#include <sstream>
#include <stdexcept>
string geocoord2string(double angle) {
if ( (angle > 180) || (angle < -180) )
throw invalid_argument("Invalid argument : angle must be beetween -180° and
180°");
int deg = int(floor(angle));
double rest = angle - deg;
int minute = int(floor( rest * 60));
rest = rest * 60 - minute;
int second = int(floor( rest * 60));
ostringstream result;
result << deg << "° " << minute << "' " << second <<"''";
return result.str();
}
double deg2rad(double deg) {
return (deg * M_PI / 180);
}
GeoPoint::GeoPoint(double lat, double lng) : lat(lat), lng(lng) {}
double GeoPoint::distance(const GeoPoint &other) {
double nDLat = deg2rad(other.lat - this->lat);
double nDLon = deg2rad(other.lng - this->lng);
double thisLatRad = deg2rad(this->lat);
double otherLatRad = deg2rad(other.lat);
double nA = pow ( sin(nDLat/2), 2 ) + cos(thisLatRad) * cos(otherLatRad) * pow ( sin(nDLon/2), 2 );
double nC = 2 * atan2( sqrt(nA), sqrt( 1 - nA ));
double nD = EARTH_RADIUS * nC;
return nD; // Return our calculated distance}
Objectif
Utiliser les fonctions C++ en python :
Projet complet disponible sur github :
https://github.com/Mappy/un-serveur-metier-en-python-cpp
$ pythonPython 2.7.3>>> import geo>>> print geo.geocoord2string(5.36)5° 21’ 36‘’
Un module python écrit en C++
D'après : https://docs.python.org/2/extending/extending.html
geomodule.cpp#include "Python.h"#include "geo.hpp"
static PyObject * geocoord2string_py(PyObject *self, PyObject *args){ double angle = 0; if (!PyArg_ParseTuple(args, "d", &angle)) return NULL; string res = geocoord2string(angle); return Py_BuildValue("s", res.c_str());}
// La liste des fonctions qu'on expose en Pythonstatic PyMethodDef geoMethods[] = { { "geocoord2string", geocoord2string_py, METH_VARARGS, "Convert a latitude or a longitude as an angle in a string in the form : d° m' s''." }, { NULL, NULL, 0, NULL } /* Sentinel */};
// La fonction initnomdumodule est appelée par l'interpréteur Python // à l'import du modulePyMODINIT_FUNC initgeo(void){ (void) Py_InitModule("geo", geoMethods);}
Avec Boost.Python ?
geomudule.cpp
… C’est tout !
#include <boost/python.hpp>#include <boost/python/module.hpp>#include "geo.hpp"
using namespace boost::python;BOOST_PYTHON_MODULE(geo){ def("geocoord2string", geocoord2string); class_<GeoPoint>("GeoPoint", init<double, double>()) .def("distance", &GeoPoint::distance) ;}
Avec Boost.Python, on peut
– Exposer des fonctions C++ en Python
– Exposer des classes C++ en Python
– Choisir quelles méthodes exposer pour une classe
– Utiliser Boost::optional pour gérer les paramètres optionnels
Packager une extension Python
Le fichier setup.py s’occupe de la compilation :
Installer avec :
python setup.py install
Ou packager :
python setup.py bdist
from distutils.core import setup, Extension
geo_module = Extension('geo', include_dirs = ['/usr/local/include',], library_dirs = ['/usr/local/lib',], extra_compile_args=['-std=c++11'], sources = ['geomodule.cpp', 'geo.cpp'])
setup (name = 'Geo', version = '1.0', description = 'A geo package from Mappy', author = 'LBS team', author_email = 'lbs@mappy.com', url = 'http://github.com/mappy', long_description = ‘A demo package with geo functions', ext_modules = [geo_module])
Focus : Gestion de la mémoire
– Un code C++ qui conserve une référence sur un objet Python doit appeler la macro Py_INCREF() pour s'assurer que l'objet ne sera pas détruit
– Il faut appeler Py_DECREF() pour libérer l'objet
Focus : Gestion des erreurs
Le code suivant lève une exception C++ :
import geo
print geo.geocoord2string(181)
Sans Boost.Python :
terminate called after throwing an instance of 'std::invalid_argument'
what(): Invalid argument : angle must be beetween -180° and 180°
Avec Boost.Python :
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: Invalid argument : angle must be beetween -180° and 180°
Boost.Python permet également d'affiner la gestion des erreurs en associant des erreurs spécifiques Python aux exceptions C++
Un serveur de distance géographiquesgeoserveur.py
A lancer par : python geoserver.py
Tester l’url : http://localhost:8888/distance/48.85,2.35/43.30,5.38
from bottle import route, run, templatefrom geo import GeoPoint
@route('/distance/<lat1:float>,<lng1:float>/<lat2:float>,<lng2:float>')def index(lat1, lng1, lat2, lng2): dep = GeoPoint(lat1, lng1) arr = GeoPoint(lat2, lng2) return template("Distance = {{distance}}", distance=dep.distance(arr))
run(host='localhost', port=8888)
Une autre approche : Producteur / Consommateur
– Le code python peut appeler des méthodes C++ à distance
– Par exemple via un bus RabbitMQ ou bien des appels distants ZMQ
– Au final, le code Python délègue l‘exécution des sections critiques à des workers C++
Les difficultés en Python quand on vient du C++
Typage dynamique
– En C++ le typage statique offre une première validation du code
– En Python, on ne détecte les erreurs de type qu'à l'exécution !
Þ il faut exécuter le code pour le tester
Þ les tests unitaires sont indispensables !
Threads
– Le Global Interpreter Lock (GIL) assure que les traitements concurrents ne se marchent pas dessus :
https://docs.python.org/2/glossary.html#term-global-interpreter-lock
http://dabeaz.blogspot.fr/2010/01/python-gil-visualized.html
– Les modules thread / threading
– encapsulent les primitives système de thread
– fournissent des primitives de synchronisation : verrous, sémaphores, etc.
– ... Mais à aucun moment deux instructions peuvent être exécutées en même temps à cause du GIL !
Þ Mieux vaut utiliser multiprocessing
Mémoire partagée
– Le module mmap (Memory-mapped file) permet de partager de la mémoire entre plusieurs process
– On peut accéder à cette mémoire comme un fichier en lecture / écriture
– On accède à cette mémoire comme un tableau de char
– Mais on ne peut pas utiliser de types complexes (listes, dictionnaires, objets définis sur mesures)
Þ Pas aussi souple qu’en C++
Conclusion
Où va Mappy ?
– Amélioration continue de l’outillage
– Nouveaux services asynchrones en Python
– … Et bientôt un nouveau serveur d'itinéraire en Python / C++ ?
Merci
Enjoy : http://fr.mappy.com/
Pierre Marquis : pierre.marquis@mappy.com
Alexandre Bonnasseau : https://github.com/abonnasseau