PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Comment figer son app hors ligne pour plus d’un mois

vendredi 25 avril 2014 à 11:35

Je sers All That Counts avec nginx, et le fichier de config est super simple :

server {
        listen       80;
        server_name allthatcounts.net;

        error_log  /var/log/nginx/error_allthatcounts.log;
        access_log  /var/log/nginx/access_allthatcounts.log;

        location / {
            root /home/allthatcounts/www/;
            gzip  on;
            gzip_http_version 1.0;
            gzip_vary on;
            gzip_comp_level 6;
            gzip_proxied any;
            gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
            gzip_buffers 16 8k;
            gzip_disable ~@~\MSIE [1-6].(?!.*SV1)~@~];
            expires modified +90d;
        }
}

En gros c’est juste du log et servir les fichiers statiques compressés avec gzip. Il n’y a rien de plus à faire parce qu’il n’y a pas de backend. Simple. Efficace.

La couille c’est que c’est un copier / coller d’un autre projet que j’ai fais sans trop réfléchir, et quand j’ai mis en prod de nouvelles modifications sur le serveurs, mon Firefox me les affichait pas. Pourtant j’avais bien modifié le manifeste, donc il aurait du tout recharcher…

Sauf que, con de ma race, j’ai copié la ligne :

expires modified +90d;

Qui dit techniquement, met en cache tous les fichiers statiques pour 90 jours. Donc aussi le manifeste. Du coup, toutes les personnes qui ont visité le site ne verront aucune mise à jour pour un bon mois et demi.

Bravo Sam.

flattr this!

Not invented here

jeudi 24 avril 2014 à 09:18

À la maison on joue beaucoup. Aux jeux vidéo, bien entendu, mais aussi à tout un tas d’autres trucs, incluant des jeux de rôles, de société, de carte… Ça joue même de la musique. Il y a un carton qui déborde de cartes magic, l’indispensable pot plein de dés 6/10/20, et des dizaines de boîtes en tout genre, parfois des titres dont moi-même je n’ai jamais entendu parlé, un piano à queue, divers lots de baffles et même des contentions psychiatriques. Pour d’autres jeux.

Je me tâte à faire une rubrique “jeu” d’ailleurs, pour introduire un jeu de plateau de temps en temps.

Évidement, à force de parties, on commence à en avoir plein le cul d’attendre le tour de l’autre, donc on joue au chrono. On a tenté le sablier, la montre, le portable, l’app, et rien n’est vraiment satisfaisant.

Que fait un programmeur dans ce cas ? Il réinvente la roue, bien sûr !

Voici donc All That Counts, une Web app qui contient :

C’est du HTML5 + Javascript, il n’y a pas de backend. Est inclus un mode offline, donc il suffit de visiter l’app une fois pour pouvoir toujours l’utiliser sauf vidange du cache. Magie de Bootstrap, ça marche sur mobile, tablette et destop. Comprendre : design responsive générique.

Sur les navs supportant l’élément audio, ça gong à la fin des timers.

La page “Count Down” est pratique pour faire sa gym, cuire ses œufs et organiser une partie avec des tours limités dans le temps pour 3 joueurs ou plus. Les widgets incluent le temps de dépassement si on a besoin de mettre des pénalités aux escargots et aux stallers.

La partie “Chrono”, c’est gadget pour moi, mais ça pourra peut être servir à certains. J’y ai collé la possibilité de cumuler des temps de tours, pour les sportifs. Je ne sais pas si c’est utile, les coureurs me diront. A la limite pour les concours d’apnée…

L’onglet “Counter” c’est pour compter les points quand on a pas de papier ou de jeton, c’est du dépannage. Ça peut servir aussi pour les gens dont le métier implique de compter des têtes de pipe comme les videurs, les hôtesses de l’air, etc.

Ce qu’on utilise le plus est le mode “Versus”, qui sert aux duels. C’est une sorte de pendule d’échecs, avec plusieurs comportements réglables. Par défaut, le passage d’un décompte à l’autre se fait automatiquement et manuellement, chaque changement de joueur impliquant une remise à zéro.

Comme il n’y a pas de backend, vous pouvez avoir le code source en faisant Ctrl + S, donc je vais pas vraiment me faire chier à le mettre sur github. Déjà, j’ai du codé en pur JS. Moi. Heureusement qu’il y a AngularJS, sinon je changeais de métier. Le déploiement, pareil, c’est Ctrl + C, Ctrl + V, donc je vais pas écrire de doc. L’utilisation, bon, c’est des clics sur des boutons labellisés…

Le code source est dispo sur github.

La seule astuce, c’est qu’en mode versus, CTRL permet de faire un switch, et SPACE permet de faire pause et resume. Je pense que vous vous en sortirez. C’est un compteur, pas une navette spatiale.

Si l’envie incroyable de rajouter des features vous prend, mettez un comment, et je me bougerais le cul pour githuber tout ça.

flattr this!

Complexité algorithmique : pourquoi tant de “n” ?

mardi 22 avril 2014 à 18:29

Que ayez eu un prof à l’ancienne durant vos études, où que vous vous soyez plongé dans des documents traitant d’optimisation, vous êtes peut être un jour tombé sur ces fameuses notations : O(n), O(1), O(log(n)), etc.

Qu’est-ce que cela signifie-t-il donc, alors, hein ?

C’est une manière de décrire l’ordre de grandeur de temps que va prendre un algo pour s’exécuter pour un nombre “n” d’éléments.

Par exemple, si je fais ceci en Python :

elements = [1, 2, 3]
for x in elements:
    print(x)
## 1
## 2
## 3

Ici, j’ai 3 éléments, donc n = 3. Mon algorithme va tous les utiliser une fois, mais pas plus d’une. Il va donc faire un nombre d’opérations proportionnel au nombres d’éléments. On note ce type de comportement O(n). Cela signifie que le temps de traitement de mon code suit à peu près “n”.

Je dis à peu près car le nombre d’éléments n’est pas uniquement ce qui va rentrer en compte. La taille des éléments, l’état de la machine au moment de l’exécution et tout un tas d’autres paramètres vont être des facteurs. Mais, globalement, je peux donner une évaluation convenable du temps que le code va prendre en notant le temps de traitement d’un seul élément, et en le multipliant par le nombre total d’éléments.

La notation O(truc), que l’on prononce “Oh de truc”, sert juste à indiquer quel type de comportement un algo a : est-ce qu’il prend du temps par rapport au nombre d’éléments ? Si oui à quel point ?

“A quel point” est une question importante, car si mon algo est celui ci:

elements = [1, 2, 3]
for x in elements:
    print()
    for i in elements:
        print(i, end="")
## 123
## 123
## 123

Alors, si n est grand, non seulement ma première boucle s’allonge, mais ma seconde boucle s’allonge aussi car j’affiche TOUS les éléments pour CHAQUE élément. Mon temps d’exécution dépend alors de “n” multiplié par lui-même : n X n. En effet, si j’ai 2 éléments, je vais faire 2 x 2 = 4 print(), si j’en ai 3, je vais faire 3 x 3 print(), etc.

Bien sûr, je pourrais faire des choses beaucoup plus compliquées qu’un print(), mais ça n’a pas d’importance. On en mesure pas le temps de tout le programme, seulement l’efficacité d’un algorithme. Ici, cela dépend du nombre d’éléments fois lui-même, soit au carré. On le note O(n²).

Il existe tout un tas de ces notations. Par exemple, 0(1) signifie un temps “constant”, c’est un abus de langage pour dire que le temps que met l’ago à s’effectuer ne dépend pas du nombre d’éléments.

Par exemple :

elements = [1, 2, 3]
print(elements[0])
## 1

Afficher le premier élément prend un temps du même ordre de grandeur – car c’est ça l’important, l’ordre de grandeur – si il y a 1 ou 10000 éléments. On note donc cet algo O(1).

Il y a des cas plus complexes. Imaginez celui-ci :

import random
 
number = random.randint(0, 100)
print("Choosing: %s" % number)
smallest = 0
biggest = 100
guess = 50
while guess != number:
    if number >= guess:
        smallest = guess
    else:
        biggest = guess
 
    guess = (biggest - smallest)//2 + smallest
    print("New guess: %s" % guess)
 
print("Last guess: %s" % guess)
## Choosing: 69
## New guess: 75
## New guess: 62
## New guess: 68
## New guess: 71
## New guess: 69
## Last guess: 69

Dans cet exemple, le nombre d’opérations dépend du nombre d’éléments “n” (ici 100) mais on divise l’interval de recherche par deux à chaque tour de boucle. On note cette complexité O(log n), puisque la fonction log illustre bien le concept d’avoir une mi-molle sur la fin de son algo :)

Il y a aussi l’inverse :

elements = [1, 2, 3, 4, 5]
copies = []
for x in elements:
    print()
    for i in copies:
        print(i, end="")
    copies.extend(elements)
## 12345
## 1234512345
## 123451234512345
## 12345123451234512345

Ici on augment la charge à traiter à chaque tour de boucle, et cette augmentation dépend du nombre d’éléments “n”. On parle d’une augmentation exponentielle de la charge de travail et on le note O(en).

A quoi ça sert ?

Essentiellement à avoir une idée d’où on met les pieds. Si vous lisez une doc, et qu’on vous dis “cet algo met un temps O(log(n))”, vous savez que même sur un grand ensemble de données, le traitement ne sera pas trop agressif. Si plus tard vous rencontrez des problèmes de perf, ce ne sera pas le premier endroit à regarder.

Par contre si vous lisez qu’un algo est O(n!) – là on tape dans les factoriels, c’est énorme – alors au premier ralentissement il faut jeter un coup d’œil sur ce bout de code.

C’est aussi utile pour comparer l’efficacité de deux implémentations.

Imaginez la structure suivante :

class Examen:
    """C'est un exemple pédagogique, ne faites pas ça chez vous les enfants"""
    def __init__(self):
        self.notes = []
 
    def ajouterNote(self, note):
        self.notes.append(note)
 
    def moyenne(self):
        total = 0
        for note in self.notes:
            total += note
        return total / len(self.notes)

Récupérer la moyenne est une opération O(n). En revanche, si on a :

class Examen:
    def __init__(self):
        self.notes = []
        self.moyenne = None
 
    def ajouterNote(self, note):
 
        if self.moyenne is None:
            self.moyenne = note
        else:
            self.moyenne = (len(self.notes)*self.moyenne + note) / (len(self.notes)+1)
        self.notes.append(note)

Là, récupérer la moyenne devient une opération O(1), on a déchargé et réparti le calcul sur l’ajout des notes. Selon que l’application va lire souvent la moyenne ou non, l’un ou l’autre algo est préférable, et la notation Big O va donner une idée duquel utiliser si on est face à la doc et pas au code, qui est généralement vachement plus compliqué que ça.

Bon, ok, dans la VVV, aucune de ces deux solutions n’est un problème, on s’en branle. Mais sur des algos plus riches, sur du matos plus limité, ou un jeu de données plus grand, c’est important. Ainsi, la doc de redis donne la notation Big O de toutes les commandes.

En Python, qui est quoi ?

Parcourir un itérable, c’est généralement du O(n), en tout cas pour les listes, les tuples, les dicos, les strings et les sets. Ajouter un élément ou en retirer un, c’est du O(1). Récupérer leurs tailles, c’est du O(1) aussi (elle est mise en cache), donc vous pouvez y aller avec len().

En revanche, l’opérateur in a un temps moyen de O(n) pour les strings, les tuples et les listes (il doit parcourir l’itérable jusqu’à trouver l’élément), et un temps de O(1) pour les sets et les dicos. Ces derniers utilisent des hash, rendant la recherche très très rapide. C’est pour cela qu’on vous dit d’utiliser la bonne structure de données pour le bon usage.

Attention cependant, c’est de l’optimisation de poil de cul, mais c’est pour la culture, O(1) ne veut pas dire “plus rapide que O(n)”. O(1) veut juste dire que le temps est indépendant du nombre d’éléments. Ainsi :

1 in [1, 2, 3] sera beaucoup plus rapide que 1 in [1, 2, 3..., 1000].

Et :

1 in {1, 2, 3} prendra un temps similaire à 1 in {1, 2, 3..., 1000}

Mais :

1 in {1, 2, 3} peut tout à faire être plus lent que 1 in [1, 2, 3]

Par contre, il est presque certain que :

1 in {1, 2, 3..., 1000} est BEAUCOUP plus rapide que 1 in [1, 2, 3..., 1000]

De plus, il y énormément de structures de données dans la stdlib Python, toutes avec des capacités différentes. Heapq assure que votre structure de données est toujours ordonnées pour un coût de O(log n) à l’insertion. Les deques sont très rapides comme FIFO/LIFO (O(1)), mais récupérer une donnée au milieu est une opération O(n). Certaines opérations, comme retirer un élément d’un type list sont étonnamment coûteuses ((O(n) dans le cas du del).

Voici quelques notations de la doc de Python.

La théorie, la pratique, et la mauvaise foi

La notation O est une bonne indication pour faire un choix d’algo ou pour commencer ses recherches de goulot d’étranglement dans un code.

Néanmoins, c’est la performance sur la papier. En pratique, on peut obtenir des résultats un peu différents, voir carrément surprenant. Il peut y avoir de multiples causes :

Donc si les perfs sont importantes, comme toujours en informatique, on a le dernier mot en mesurant soi-même.

flattr this!

Le manifeste du cache du mode hors ligne pour HTML5

dimanche 20 avril 2014 à 14:17

La bataille app native VS site responsive va faire rage pendant pas mal de temps, et pour le moment les apps gagnent : performances plus élevées, meilleures intégration visuelle dans l’OS, accès à une API plus riche… Les utilisateurs les préfèrent, et du coup les pros sont obligés de se les coltiner. C’est chiant, mais c’est la réalité du terrain pour les dev sur mobile.

Mais pour les sites Web ou les apps simples, il est super intéressant d’exploiter les capacités HTLM5 au max pour une obtenir une expérience plus “app” et moins “site web”.

Parmi ces possibilités : le mode hors ligne. D’un côté, il y a le stockage des données dans le navigateur, mais on vous en a déjà parlé.

De l’autre, il y a le cache des ressources. Cela consiste à déclarer quels fichiers (html, css, js, images, fonts, n’importe quoi…) garder en mémoire afin de les charger directement depuis le disque dur au lieu de le faire en ligne.

Pour cela, il faut déclarer un manifeste dans son HTML :

<html manifest="cache.manifest">

Ensuite, on créer le fichier cache.manifest dans son projet, qui est un fichier de texte simple.

Il faut le faire servir avec le mime-type

text/cache-manifest

sinon ça ne marche pas. Si vous le nommez

*.manifest

et que vous le servez avec un serveur de dev, ça marchera tout seul. Pour la prod, il faut le spécifier à votre serveur. Par exemple avec nginx, il faut éditer le fichier /etc/nginx/mime.types et y ajouter :

text/cache-manifest                   manifest;

Pour apache, c’est un truc du genre dans le .htaccess:

AddType text/cache-manifest .manifest

Ensuite, le manifeste ressemble à ça :

CACHE MANIFEST
# 2014-04-20 13:25:00

# Toutes les ressources à sauvegarder en local. Le navigateur
# va toujours chercher ces ressources sur le disque.
# Si on est déconnecté, la page index.html, ses styles et javascript
# s'afficheront donc quand même.
CACHE:
index.html
/favicon.ico
stylesheet.css
images/logo.png
scripts/main.js
fonts/font.woff

# Ressources qui ne sont chargées que si on est en ligne.
# Par exemple, on charge le module de stats de visiteurs que si on est
# en ligne car impossible de compter les vues sans le serveur.
NETWORK:
js/visits.js

# Ressources alternatives si les précédentes sont inacessibles.
# Par exemple, pour afficher un point rouge si on est hors ligne
# et un point vert si on est en ligne :
# images/offline.png sera servi si images/online.png est inaccessible
FALLBACK:
images/online.png images/offline.png

Notez le commentaire # 2014-04-20 13:25:00 tout en haut. C’est
une convention qu’on utilise pour donner la dernière date de modification des
fichiers cachés. En effet, les fichiers de la section CACHE ne seront
pas rechargés tant que le manifeste n’a pas été modifié.

Cela veut dire que si vous modifiez index.html, l’utilisateur
ne verra pas la modification. Mais si vous changez la date du fichier manifeste,
le fichier est modifié, et le navigateur rechargera donc toutes les ressources
qu’il a mis en cache. Ainsi, vous permettez aux utilisateurs de voir les
ressources cachées qui ont été modifiées.

flattr this!

Générer des données factices avec faker

jeudi 17 avril 2014 à 10:30

faker fait partie de ses libs que j’ai toujours voulu écrire sans jamais prendre le temps de le faire. Comme arrow par exemple. Et puis un jour quelqu’un le fait, et je suis à la fois soulagé de ne pas avoir tenté de le faire (au risque de ne pas réussir aussi bien) et un peu déçu d’être passé à côté de la bonne idée.

Le principe de la lib est très simple : générer des données bidons. Noms, numéros de téléphone, adresses physiques ou email… C’est utile pour tout un tas de choses :

Faker est déjà très simple à utiliser :

>>> from faker import Faker
>>> f = Faker()
>>> f.phone_number()
u'+69(4)8833689405'
>>> f.phone_number()
u'1-201-240-9452'
>>> f.phone_number()
u'+95(8)7680219065'
>>> f.phone_number()
u'754-833-9664x654'
>>> f.email()
u'bentley.gaylord@batz.org'
>>> f.email()
u'ludwig.rohan@adamskoch.info'

Et surtout déjà très riche. Il y a plus d’une centaine de choses que vous pouvez générer. Quelques exemples :

faker est très flexible. En effet, beaucoup d’éléments existent en plusieurs versions ou de manière composée et utilisables séparément. Ainsi, on peut prendre l’IPV4 ou V6, choisir l’adresse entière ou le code postal seul, récupérer le nom complet, ou juste le prénom :

>>> f.name()
u'Olin McCullough'
>>> f.first_name()
u'Brandie'

Mais en plus, faker est localisé :

>>> f = Faker(locale="fr_FR")
>>> f.name()
u'Martine de la Petitjean'

Enfin, on peut créer ses propres générateurs et fournisseurs de données au besoin.

Comme dirait Max, c’est d’la balle baby !

flattr this!