PROJET AUTOBLOG


Sam et Max

source: Sam et Max

⇐ retour index

Quelques innovation de Python 3 backportées en Python 2.7

lundi 5 novembre 2012 à 12:36

Comme nous l’avons vu avec les vues ou les collections, Python 2.7 vient avec pas mal de bonus issus directement de la branche 3. En voici quelques autres. Tout ceci n’est bien sûr ni nouveau ni exhaustif, mais je m’aperçois que peu de personnes le savent.

Une notation littérale pour les sets:

>>> {1, 2} == set((1, 2))
True

Une syntaxe pour les dictionnaires en intention:

>>> d = {chr(x): x for x in range(65, 91)}
>>> d
{'A': 65, 'C': 67, 'B': 66, 'E': 69, 'D': 68, 'G': 71, 'F': 70, 'I': 73, 'H': 72, 'K': 75, 'J': 74, 'M': 77, 'L': 76, 'O': 79, 'N': 78, 'Q': 81, 'P': 80, 'S': 83, 'R': 82, 'U': 85, 'T': 84, 'W': 87, 'V': 86, 'Y': 89, 'X': 88, 'Z': 90}

Imbriquer with:

Avant il fallait utiliser nested() ou imbriquer à la main

with open('fichiera') as a:
    with open('fichiera') as b:
        # faire un truc

Maintenant on peut faire:

with open('fichiera') as a, open('fichiera') as b:
    # faire un truc

Rien à voir, mais toujours sympa. timedelta a maintenant une méthode total_seconds() qui retourne la valeur de la durée en seconde. En effet, l’attribut seconds ne retourne que ce qui reste en seconde une fois qu’on a retiré les jours:

>>> from datetime import timedelta
>>> delta = timedelta(days=1, seconds=1)
>>> delta.seconds
1
>>> delta.total_seconds()
86401.0

Notez qu’il n’y a toujours ni attribut minutes, ni heures.

Le module unittest gagne une pléthore d’améliorations, et notamment:

L’utilisation de assertRaises comme context manager:

with self.assertRaises(KeyError):
    {}['foo']

Et un bon gros nombres de méthodes:

assertIsNone() / assertIsNotNone(), assertIs() / assertIsNot(), assertIsInstance() / assertNotIsInstance(), assertGreater() / assertGreaterEqual() / assertLess() / assertLessEqual(), assertRegexpMatches() / assertNotRegexpMatches(), assertRaisesRegexp(),
assertIn() / assertNotIn(), assertDictContainsSubset(), assertAlmostEqual() / assertNotAlmostEqual().

Enfin format() commence à devenir une alternative valable à % car il propose maintenant des marqueurs sans noter d’index:

>>> "{}, puis {} et finalement {}".format(*range(3))
'0, puis 1 et finalement 2'

Et il ajoute le séparateur des milliers au mini-langage de formatage, mais pour la virgule uniquement. Par exemple, si avoir un nombre de 15 caractères minimum formater en tant que float, avec deux chiffres après la virgules, et donc les milliers sont groupés à l’américaine:

>>> '{:15,.2f}'.format(54321)
'      54,321.00'

Qu’est-ce qu’une structure de données ?

dimanche 4 novembre 2012 à 23:33

Dans certains des articles, on utilise ce terme. Et il est très répandu également sur le Web, sous cette forme ou dans sa version angloclassos: data structure.

Une structure de données est simplement du code dont le but est de contenir des données et de les représenter sous une certaine forme:

Les structures de données sont donc des conteneurs, mais qui sont liés à une certaine logique de manipulation: on peut faire des append() et des pop() sur une liste, des get() et setdefault() sur un dictionnaire, et des parent(), left() et right() sur un arbre binaire. Dans des langages qui ne sont pas complètement orientés objet comme PHP, on a des fonctions pour manipuler ces structures de données, par exemple
array_pop() permet de manipuler Array().

Or, vous pouvez coder votre propre structure de données, et c’est d’ailleurs ce que vous faites quand vous créez un objet.

Vous pouvez apprécier maintenant la question suivante avec une certaine ironie: qu’est-ce qui n’est PAS une structure de données ? Dans les langages comme le C, une fonction n’est pas une structure de données: elle ne contient rien, c’est une suite d’instructions. Mais dans le langage Python, une fonction est une structure de données car c’est un objet: il contient des références vers son code, ses arguments, et tout un tas de metadata.

Pour se simplifier la vie, on parlera de structure de données pour les objets qui ont été créés avec pour but de contenir et de manipuler des données à travers eux (liste, tuple, set, dict, strings, etc), et non pas des objets dont leur nature a pour effet secondaire d’être une structure de données (fonctions, objets métiers, classes, etc).

Notez bien que les structures de données sont très nombreuses: il n’y a pas que les tuples et les sets dans la vie. Rien qu’en Python, on a des arrays, des string buffers, des heap queue, et tout un tas de joyeusetés.

Les vues sur des collections en Python

samedi 3 novembre 2012 à 19:57

Python 3 introduit de nombreux changements qui ont été backportés dans Python 2.7. Parmi eux, les vues, qui sont un concept assez mal expliqué dans la documentation standard.

Dictionary views

Quand on voulait travailler sur les valeurs d’un dictionnaire en Python, on avait deux choix:

Les vues sont une solution intermédiaire: ce sont des objets qui prennent peu de mémoire, mais qui peuvent être lus plusieurs fois.

Exemple:

>>> scores = {'foo': 1, 'bar': 0}
>>> val = scores.viewvalues()
>>> print val
dict_values([1, 0])
>>> 1 in val
True
>>> [x * 2 for x in val]
[2, 0]

Contrairement à une liste, les vues issues d’un dictionnaire ne supportent pas le slicing ou l’assignation et il n’y a aucune garantie d’ordre des éléments. De plus, elles ne peuvent être modifiées.

Bref, une vue ne contient rien, c’est juste un objet qui, quand on accède à son contenu, va le chercher dans le dictionnaire et vous le retourne. C’est ce qu’on appelle un objet proxy: il vous donne l’illusion d’accéder directement aux données pour vous faciliter la vie, généralement en vous les présentant sous une forme différente: ici un itérable.

On peut récupérer des vues pour les valeurs, mais également pour les clés et les couples clés / valeurs. Ces deux types de vues se comportent en plus comme des sets:

>>> scores.viewitems()
dict_items([('foo', 1), ('bar', 0)])
>>> scores.viewkeys() | [3,]
set([3, 'foo', 'bar'])

Puisqu’il est rare d’avoir besoin d’une vraie liste, et comme les vues sont une très bonne alternative aux générateurs, dict.values et consorts retournent des vues en Python 3.

Maintenant vous allez me dire “Mais si les vues sont une si bonne alternative aux générateurs, pourquoi on ne remplace pas tous les générateurs par des vues ?”.

Tout simplement parce que ce n’est pas possible. Un générateur est un mécanisme standard qui permet de produire des valeurs une par une. N’importe qui peut créer un générateur, car c’est un concept portable d’un problème à un autre. On peut l’appliquer à de nombreuses choses: algorithme, flux de données, fichier, etc.

Une vue n’est qu’un proxy qui permet de voir une structure de données sous une autre forme. Il faut coder une vue par type de structure de données, car la vue va chercher les données dans cette structure quand on lui demande. Le code est donc différent à chaque fois.

Python ne permet pas de créer soi-même des vues, mais créer un proxy, c’est à dire un objet qui retourne les valeurs d’un autre objet quand on l’interroge, peut se faire à la main dans tout langage de programmation. Ainsi vous pourriez créer un proxy qui ressemble a une vue des clés d’un dico très simplement:

class keyview(object):
 
    def __init__(self, d):
        self.d = d
 
    def __iter__(self):
        return self.d.iterkeys()
 
>>> view = keyview(scores)
>>> for x in view:
...     print x
...     
foo
bar
>>> list(view)
['foo', 'bar']
>>>

L’implémentation réelle de Python (en C…) ne fait pas vraiment grand chose de plus, juste un travail d’optimisation pour être plus rapide.

memoryview

Les memory views suivent le même principe, mais appliqué à toute structure de données qui supporte le buffer protocole (un certain nombre de méthodes avec un nom et un comportement défini par ce protocole) comme celles trouvées dans le module struct ou array. La structure de données la plus connue qui suit le buffer protocole est la chaîne de caractères.

>>> s = 'Sam & Max eat the road with a Github fork'
>>> ms = memoryview(s)
>>> ms[-1]
'k'
>>> ms[:9]
<memory at 0x25ded60>
>>> ''.join(ms[:9])
'Sam & Max'

Le principal intérêt de la memory view appliquée aux strings, c’est que tout slicing retourne une nouvelle memory view. On peut donc travailler sur des parties de la chaînes sans créer une nouvelle chaîne en mémoire.

En revanche, les chaînes unicodes ne sont pas supportées. Il vous faudra jouer avec encode() et decode().

Cookies, sessions et redirections dans un middleware Django

vendredi 2 novembre 2012 à 19:31

En Django les sessions utilisent des cookies, et il n’y a pas de fallback possible sur un SESSION_ID passé dans l’URL comme le fait par exemple PHP (il y a des apps pour ça, mais la pratique est considérée peu sécurisée de toute façon).

Or, comme HTTP est stateless, les cookies sont échangés en permanence, on les reçoit par la requête, et on les envoie avec les réponses. Ajoutez à cela qu’un client peut choisir de désactiver le support de cookie, et vous avez là un merveilleux casse-tête.

Imaginez: vous vérifiez que l’utilisateur possède une valeur dans les cookies. Si ce n’est pas le cas, vous mettez la valeur dans le cookie et vous redirigez sur la page en cours. Ainsi, il retourne sur la même page, avec le cookie. Mais si il a les cookies désactivée, il va revenir sans le cookie, et vous allez le rediriger, encore, et encore.

Et oui, on peut faire des boucles infinies avec HTTP !

Tester si les cookies sont disponibles

Le seul moyen de s’en sortir dans un site qui ne peut pas fonctionner sans cookie (ce qui est le cas de toute partie d’une site qui demande une authentification) est d’utiliser 3 vues.

La vue normale qui demande une session, et donc un cookie.
La vue qui remplit la session avec les informations utiles (par exemple la vue d’authentification, ou autre).
La vue vers laquelle on renvoie si il n’y a pas de cookie (une page expliquant qu’il faut activer les cookies si on veut accéder à la partie authentifiée du site).

Voilà à quoi ressemble le mécanisme dans sa plus simple expression:

from django.shortcuts import redirect
from django.http import HttpResponse
 
 
# '^nocookies/'
def nocookies(request):
    """
        Page expliquant avec pédagogie que les cookies sont nécessaires
        à l'accès à la partie authentifiée du site.
    """
    return HttpResponse('Use cookies, you bastard')
 
 
# '^setinfos/'
def setinfos(request, next):
    """
        Page qui vérifie si les cookies sont disponibles, et si oui, remplit
        la session d'infos importantes, puis redirige vers la page précédente.
 
        Si les cookies ne sont pas disponibles, on redirige vers la page
        d'explication.
    """
 
    # On vérifie la présence du cookie de test
    # Il atteste que les cookies marchent
    if not request.session.test_cookie_worked():
        return redirect('/nocookies/')
 
    # On nettoie le cookie de test, on va pas pourrir le navigateur du user :-)
    request.session.delete_test_cookie()
    # On met les infos en sessions
    request.session['trucimportant'] = 'Super Important !'
 
    # On redirige vers la page qui a redirigé vers cette page
    # Si la page précédente n'a pas précisé ou rediriger en querystring,
    # on redirige vers l’accueil
    return redirect(request.GET.get('next', '/'))
 
 
# /profile
def profile(request):
    """
        Page qui a besoin d'informations en session, et qui redirige vers
        la page qui écrit ces infos en cas d'absence de celles-ci.
    """
 
    important = request.session.get('trucimportant', None)
 
    if not important:
        # on met le cookie de test ici, car on veut vérifier sa présence
        # dans la prochaine vue
        request.session.set_test_cookie()
        # On redirige vers la vue qui met les infos dont on a besoin en session
        # en précisant vers où rediriger une fois les infos mises en place
        return redirect('/setinfos/?next=%s' % request.get_full_path())
 
    return HttpResponse(important)

Pour la page pédagogique, ma technique favorite consiste à mettre une courte explication avec un lien vers la page que Google utilise pour expliquer comment activer les cookies: cette page est à jour pour tous les browsers et est disponible dans de nombreuses langues (crédit à 37signals qui font ça pour le javascript). Je met aussi un lien vers la page qui set les infos en session avec l’URL de redirection en paramètre, car sinon l’utilisateur ne saura pas retourner à son point de départ. Le texte doit être court, clair, et pas perturbant.

Et dans un middleware…

Mais le plus fun, c’est quand on fait tout ça dans un middleware. Car on a pas le loisir d’avoir plusieurs vues avec lesquelles jouer. Que faire alors ? Et bien on va utiliser HTTP jusqu’au bout en stockant la sémantique dans les URLs, et en la récupérant directement dedans.

C’est ce que je fais dans le middleware d’autologging de Django quicky:

class AutoLogNewUser(object):
 
 
    CALLBACK = setting('AUTOLOGNEWUSER_CALLBAK', None)
 
 
    def process_request(self, request):
 
 
        # on arrive dans ce if seulement si on a détecté avant pas 
        # de données dans la session et redirigé sur cette URL 
        if 'django-quicky-test-cookie' in request.path:
 
            # on check que le test cookie a marché, et sinon, on redirige
            # sur la page pédagogique
            if not request.session.test_cookie_worked():
                return render(request, 'django_quicky/no_cookies.html',
                              {'next': request.GET.get('next', '/')})
 
            request.session.delete_test_cookie()
 
            first_name = iter(NameGenerator()).next().title()
            username = "%s%s" % (first_name, random.randint(10, 100))
            user = User.objects.create(username=username,
                                       first_name=first_name)
            request.session['django-quicky:user_id'] = user.pk
 
            # tout a bien marché, on redirige vers l'url précédente
            next = request.GET.get('next', '/')
            if self.CALLBACK:
                res = self.CALLBACK(request)
            return redirect(res or next)
 
 
        # ici is_authenticated() ne marche pas si pas de cookie
        if not request.user.is_authenticated():
 
            user_id = request.session.get('django-quicky:user_id', None)
 
            # rien en session ? on redirige sur l'url test de cookie
            if not user_id:
 
                request.session.set_test_cookie()
                return redirect('/django-quicky-test-cookie/?next=%s' % request.path)
 
            request.user = User.objects.get(pk=user_id)

Et si on veut stocker tout ça dans des cookies, et pas en session ?

On est très tenté de faire un truc comme ça:

request.COOKIES['trucimportant'] = 'Super Important !'

Mais ça ne peut plus marcher que de faire ça:

request.POST['trucimportant'] = 'Super Important !'

En effet, les cookies sont justes des infos qu’on s’envoie. Il n’y a pas de lien entre ceux qu’on reçoit et ceux qu’on envoie, et là on écrit dans le dictionnaire de ceux qu’on reçoit.

Donc pour envoyer un cookie, il faut le faire dans la réponse:

response = render_to_response(...)
response.set_cookie("trucimportant", 'Super Important !')
return response

Google ne sait plus référencer correctement les sites, la preuve en images

jeudi 1 novembre 2012 à 15:33

Article un peu provocateur certes mais les résultats Google sont de plus en plus médiocres, j’en veux pour preuve le site de vos dévoués serviteurs, sam & max. En fouinant dans les mots-clefs qui vous font venir ici je m’aperçois que pour certains on ressort non seulement sur la première page des résultats Google mais qu’en plus on rafle la place à des sites institutionnels.

Deux exemples au menu, je précise que les tests ont été fait depuis différents navigateurs, sur des IP différentes, sans être loggué avec un quelconque compte Google.

1. Que les diététitiens me lèchent les couilles!
Je rafle la première place car je suis le plus beau, c’est normal j’ai pondu un seul article sur comment bien manger et paf nous voilà sur la première page devant mangerbouger.fr.

10 000$ le backlink, qui n'en veut ?

En cherchant sur pagerank.fr j’ai constaté que mangerbouger.fr avait un pagerank de 6 (sametmax.com a un pagerank de 2) et 1080 bakclinks contre une 20aine pour S&M. C’est quand même un comble, honte sur eux…

2. Youporn a qu’à bien se tenir!
On domine les résultats des recherchent sur le sexe au japon. Là encore un article certes avec du contenu mais pas de quoi fouetter une naine obèse unijambiste, arrive en premier pour pas mal de mots clefs qu’un webmaster de site de boules voudrait bien avoir (sexe japon, sex japon, sexe japonais, etc..)

on fait moins les malins là !

Je pense qu’un mec qui va taper ce genre de mot-clef (safesearch désactivé) a déjà la nouille à la main prêt à solliciter les actions de la companie Kleenex et ne s’attend pas à tomber sur un site comme le nôtre.

Bientôt quand on va taper “unijambiste fisting” on va tomber sur un site de prothèses vous allez voir…

Je nous prends comme exemple mais ça commence à me le faire de plus en plus, les recherches sont beaucoup moins sympas qu’avant je trouve, il y a aussi les sites comme Wikipedia qui tombent quasi systématiquement en premier alors que presque n’importe qui peut modifier un article sournoisement pour induire le lecteur en erreur.

Conclusion:
Alors oui on pourrait croire que c’est cool d’avoir un petit site perso comme S&M qui sort pour “comment bien manger” plutôt qu’un site institutionnel, que ça permet à des sites de sortir du néant face à des mastodontes mais faudrait pas que ça tombe dans le n’importe quoi non plus.
En fait la première page des résultats de Google est de moins en moins utile je trouve, soit on croise des sites qui n’ont rien à voir avec la choucroute soit on tombe sur des géants comme Wikipedia et dont on s’en branle, voire même depuis quelques temps Google nous refourgue ses propres services (traduc, email et Cie).
Faudrait sortir un plugin qui renvoit directos sur la 2ème page ^^

HA! On vient de me signaler qu’un agent de Google vient de passer sur S&M…
En plongée…. 2000 mètres…