views – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Les vues sur des collections en Python http://sametmax.com/les-vues-sur-des-collections-en-python/ http://sametmax.com/les-vues-sur-des-collections-en-python/#comments Sat, 03 Nov 2012 18:57:02 +0000 http://sametmax.com/?p=2785 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:

  • faire dict.values() et récupérer une liste entière. Créant une liste entière en mémoire.
  • faire dict.itervalues(), et récupérer un générateur. Mais qui ne peut être lu qu’une fois.

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]

>>> ''.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().

]]>
http://sametmax.com/les-vues-sur-des-collections-en-python/feed/ 15 2785
Des vues normales aux vues génériques Django http://sametmax.com/des-vues-normales-aux-vues-generiques-django/ http://sametmax.com/des-vues-normales-aux-vues-generiques-django/#comments Wed, 15 Aug 2012 18:18:20 +0000 http://sametmax.com/?p=1715 ce code dans le cadre de notre opération père castor, commente nous un snippet. Je me suis dis que j'allais faire d'une pierre deux coups et trois mouvements.]]> Nous avons reçu plusieurs demandes d’explication des class based views et une explication de ce code dans le cadre de notre opération père castor, commente nous un snippet.

Je me suis dis que j’allais faire d’une pierre deux coups et trois mouvements.

Rappel sur les bases des vues

Une vue, c’est une fonction ORDINNAIRE qui prends une requête en paramètre et qui revoie une réponse.

Si vous avez un urls.py qui ressemble à ça:

from mon_app.views import home, tous_les_utilisateurs
from django.contrib.auth.views import login

urlpatterns = patterns('',
    url(r'^/', home),
    url(r'^login/', login),
    url(r'^list/', tous_les_utilisateurs),
)

Et une vue dans views.py qui ressemble à ça:

from django.shortcurts import render
from django.contrib.auth.models import User

def tous_les_utilisateurs(request):
    context = {"utilisateurs": User.objects.all()}
    return render(request, "tous_les_utilisateur.html", context)

Voilà ce qui va se passer si l’utilisateur visite http://monsite.com/list/:

Fonctionnement d'une vue Django

L'important ici est de comprendre que tout ça fonctionne à base d'arc-en-ciels distillés

Seulement voilà, vous allez vouloir rajouter des trucs, genre, la pagination.

Et votre vue va se changer en ça:

def tous_les_utilisateurs(request):

    # montrer 25 users par page
    paginator = Paginator(User.objects.all(), 25)

    # récupérer la page en cours
    page = request.GET.get('page')
    try:
        users = paginator.page(page)
    except PageNotAnInteger:
        # si la page n'est pas un entier, on affiche la page un
        users = paginator.page(1)
    except EmptyPage:
        # Si la page déborder, on affiche la dernière page
        users = paginator.page(paginator.num_pages)

    context = {"utilisateurs": users, "page": page}

    return render(request, "tous_les_utilisateur.html", context)

Et vous allez faire ça pour 512 vues. Des listings, c’est pas ce qui manque. Prendre un objet et afficher son détail, le modifier, vérifier si il existe, valider un formulaire, faire un listing d’objets, tout ça sont des tâches très courantes.

Pour cette raison, Django fournit des vues génériques, des fonctions NORMALES, mais qui sont paramétrables afin de pouvoir faire des listings, des validations de formulaire, etc, de tout et n’importe quoi. Sans avoir à écrire le code.

Les vues génériques sous forme de fonction

Une vue générique n’a rien de magique, c’est une vue ordinaire, seulement elle a des tas et des tas d’arguments pour pouvoir en faire ce qu’on veut. Chaque vue générique à un but précis. Par exemple, la vue générique object_list à pour but de créer des listings de n’importe quel objet, avec pagination.

Si on devait écrire notre vue précédente avec une vue générique, ça donnerait ça:

from django.views.generic.list_detail import object_list

def tous_les_utilisateurs(request):
    return object_list(request, queryset=User.objects.all(), paginate_by=25,
                       template_name="tous_les_utilisateur.html",        
                       template_object_name="utilisateurs")

object_list est une vue normale, donc elle attend en paramètre un objet request, et retourne une réponse. Ainsi, on lui passe request de tous_les_utilisateurs, et on récupère sa réponse, qu’on retourne comme si c’était la notre.

La différence, c’est que object_list à plein de paramètres en plus:

  • queryset est le paramètre qui dit quel objet lister;
  • template_name est le paramètre qui dit quel template utiliser;
  • template_object_name est le paramètre qui dit quel nom donner à la liste d’objets dans le template.

object_list s’occupe du reste, notamment de la pagination et de la gestion des erreurs. Elle frabrique l’objet context et le passe au template. Il n’y a donc plus qu’à écrire le template.

C’est d’ailleurs la partie difficile au début: comment on écrit le template ? En fait tout à fait normalement, il faut juste savoir quelles variables (et ce quelles contiennent) sont mises à disposition dans le template par la vue générique. Il faut lire la doc quoi :-) Ou faire comme moi et utiliser django-template-repl qui est une sorte de pdb pour template.

Dans notre cas, le template va contenir la variable pag_obj (qui est l’objet de pagination) et la variables utilisateurs qui est une liste d’objets User.

Les vues génériques suivent des conventions de nommage, et si on s’y tient, on peut même raccourcir le code encore plus:

def tous_les_utilisateurs(request):
    return object_list(request, queryset=User.objects.all(), paginate_by=25)

Mais dans ce cas il faut nommer obligatoirement notre template [app_label]/[model_name]_list.html (dans notre casmonapp/user_list.html) et dans ce template, récupérer la liste d’utilisateurs dans la variable de nom object_list (facile à retenir, c’est le même nom que la vue).

Comme toutes les vues génériques, object_list est très paramétrable. Supposons qu’on veuille rajouter la date du jour dans la page, il suffit de le rajouter dans le template et de faire ceci:

def tous_les_utilisateurs(request):
    aujourdhui = datetime.datetime.now()
    return object_list(request, queryset=User.objects.all(),
                       template_name="tous_les_utilisateur.html",
                       paginate_by=25,
                       template_object_name="utilisateurs",
                       extra_context={'date_du_jour': aujourdhui})

Et le template aura automatiquement accès à la variable date_du_jour.

Les vues génériques font gagner beaucoup de temps, mais elles font un peu peur au début car on ne sait pas trop ce qu’elles font à l’intérieur, ni comment faire le template.

Pour cette raison, je ne recommande pas aux débutants de les utiliser, faites des vues à la main d’abord, et quand vous aurez assez de code derrière vous pour comprendre les points communs entre toutes ces vues, vous saurez exactement ce que fait une vue générique: ce que vous avez réécrit 100 fois.

En cas de doute, la doc de Django explique comment utiliser les vues génériques, et surtout, une liste des vues génériques disponibles pour chaque cas d’utilisation.

Parmi les vues génériques les plus utiles:

  • object_list: lister un objet et paginer la liste
  • direct_to_template: retourner juste le template sans rien d’autre
  • redirect_to: une redirection pure et simple
  • object_detail: afficher un objet en particulier, ou une 404 si il n’existe pas
  • create_object: afficher un formulaire pour créer une objet
  • update_object: afficher un formulaire pour modifier un objet
  • delete_object: supprimer un objet, avec une confirmation

Rappelez-vous, les vues génériques sont des vues normales, et les vues sont des fonctions ordinnaires. La seule chose dont il faut se souvenir: elles acceptent en paramètre un objet Request, et retourne un objet Response. On peut donc faire tout ce qu’on fait d’habitude avec les fonctions: forcer des arguments, utilisers plusieurs vues dans une, faire des callbacks, etc.

Mais le plus important, on peut donc mettre une vue générique directement dans urls.py. Ainsi ceci:

from mon_app.views import tous_les_utilisateurs
urlpatterns = patterns('',
    url(r'^list/', tous_les_utilisateurs),
)

Peut tout à fait être remplacé par cela:

import datetime
from django.views.generic.list_detail import object_list
urlpatterns = patterns('',
    url(r'^list/', object_list, {"queryset": User.objects.all(),
                                 'template_name': 'tous_les_utilisateur',
                                 "template_object_name: "utilisateurs,
                                 "paginate_by": 25,
                                 extra_context={'date_du_jour': datetime.datetime.now})
)

object_list étant une vue normale, elle peut être appelée directement par urls.py. Il faut juste s’assurer de mettre le dictionnaire de paramètres additionels avec pour qu’elle soit configurée correctement. Du coup, on a zero code dans views.py.

On utilise rarement cela pour des vues comme object_list (ça pourrit un peut le urls.py qui doit rester facile à lire). Par contre, les vues direct_to_template et redirect_to sont très souvent utilisées de cette manière.

En conclusion, notez que vous pouvez très bien créer vos propres vues génériques. Une vue générique est juste une vue normale avec plein de paramètres pour qu’elle soit très souple et réutilisable.

Les vues génériques sous forme de classes

Les class base views, ou CBV, sont exactement la même chose que précédément, mais sous forme de classe. Elles se configurent plus de manière déclarative. Ainsi notre exemple précédent se ferait ainsi:

from django.views.generic import ListView
from django.contrib.auth.models import User
class TousLesUtilisateurs(ListView):
   context_object_name = "utilisateur"
   queryset = User.objects.all()
   template_name = "tous_les_utilisateurs.html"

Et dans urls.py:

from mon_app.views import TousLesUtilisateurs
urlpatterns = patterns('',
    url(r'^list/', TousLesUtilisateurs.as_view()),
)

Néanmoins, quand il faut rajouter des valeurs dans le context, ça se gate. En effet, une classe, c’est de l’objet, qui dit objet, dit (sauf prototypage) héritage, et héritage, dit overriding.

Les class based views sont bien faites: tout leur comportement peut être configuré. Le problème, c’est que ça suppose que vous sachiez exactement comment elles marchent. Par exemple, pour rajouter un objet dans le context, il faut overrider la méthode get_context_data:

import datetime

from django.contrib.auth.models import User
from django.views.generic import ListView

class TousLesUtilisateurs(ListView):
   context_object_name = "utilisateur"
   queryset = User.object.all()
   template_name = "tous_les_utilisateurs.html"

   def get_context_data(self, **kwargs):
        # qui dit overriding, dit appel de la méthode parent...
        context = super(TousLesUtilisateurs, self).get_context_data(**kwargs)
        # et on rajoute la date du jour dans le context
        context['aujourdhui'] = datetime.datetime.now()
        # le context retourner sera automatiquement injecté dans le template
        # dans la méthode render(), que vous ne voyez pas...
        return context

L’idée derrière ces classes, c’est que vous pouvez réutiliser du code bien plus facilement: on peut faire hériter des vues les unes des autres, et overrider seulement certaines méthodes. D’ailleurs, elles sont très bien pensées, et il y a des hooks partout.

Par exemple, si vous voulez tous les users, vous pouvez faire:

class TousLesUtilisateurs(ListView):
   model = User

Si vous voulez un filtrage particulier, vous pouvez faire:

class TousLesUtilisateurs(ListView):
   querset = User.object.filter(truc=machin)

Et si vous voulez un filtrage dynamique vous pouvez faire:

class TousLesUtilisateurs(ListView):

    def get_queryset(self):
        """
            Listing de tous les users, ou seulement de ceux qui ont accès
            à l'admin Django
        """
        # self.args[0] suppose que l'url prend un paramètre, ce que nous 
        # n'avons pas fait.  C'est pour l'exemple.
        if self.args[0] == "staff":
            return User.objects.filter(is_staff=True)
        return User.object.all()

Et la chaîne logique c’est: get_queryset est appelé automatiquement dans get_context_data, si c’est le votre, il override, sinon il essaye cls.queryset, et si il n’existe pas, il essaye de créer le queryset à partir de cls.model. Et sinon il fait une erreur.

C’est logique. Certes.

Mais c’est chiant.

Car c’est comme ça pour tout: il faut tout connaitre. Par coeur. Pareil pour les formulaires, les update des objets, etc. Et ensuite se rajoute la complexité des mixins. En plus la doc est super nulle sur ce point. Et il y a une sacrée liste de CBV.

Et franchement, le get_context_data, vous trouvez ça lisible ? Je vous garantie que quand on tombe sur une méthode de 10 lignes overridées d’une classe custom qui utilise 2 mixins sur un code qu’on a laissé depuis 3 mois, ça fait tout drôle.

Pour cette raison, je recommande ne ne PAS utiliser les CBV. Pour votre bien être, et celui de vos collègues. Certains de mes clients m’imposent d’ailleurs de ne pas le faire, contractuellement. Max en a horreur. Et après un an de mise en production de mes premières CBV, je confirme: le temps et la lisibilité perdus ne sont pas compensés par le gain en flexibilité.

Malheureusement les CBV sont la nouvelle manière de faire, les vues génériques sous forme de fonctions ont été marquées “deprecated”. Néanmoins, et tant que c’est possible, je recommande de continuer à les préférer aux CBV: comme les vues fonctions sont des vues et fonctions ordinnaires, il sera toujours facile de récupérer le code de Django qui font ces vues, même si elles sont retirées des version futures.

Explication du code

Bah, oui, parce qu’à la base, je devrais expliquer ce code, souvenez-vous…

Code 1

urlpatterns = patterns('',
    # On associe la vue "listview" à l'url "monsupersite.com/list/". Cette
    # association est appelée une route. Une route est donc une ligne dans
    # "urlpatterns" de urls.py.
    # {} est un dictionnaire de paramètres optionels
    # 'myobject_list' est le nom (appelé parfois "urlname") qu'on donne à
    # cette route
    (r'^list/', listview, {}, 'myobject_list'),
)

Avec ce model:

## models.py
from django.contrib.auth.models import User

# MyObject est un model tout simple avec une foreign key qui pointe vers
# le modèle User fournit par l'application "auth" de Django
# User est un modèle tout fait pour gérer les utilisateurs, mots de passes
# préférence, permissions, etc.
class MyObject(models.Model):
    ...
    author = models.ForeignKey(User)

Et cette vue:

## views.py
# on protège cette vue pour qu'elle ne soit accessible que pour les
# utilisateurs authentifiés
@login_required
def listview(request,
          queryset=MyObject.objects.all(),
          template_name='myproject/myobject_list.html'):

    # notez que l'auteur à ajouté des paramètres additionels à sa propre
    # vue: ils lui permettront d'avoir une vue plus souple et réutilisable

    # on récupère l'utilsiateur courant (request.user)
    # et on filtre MyObject.objects.all() pour qu'il ne contienne que les
    # objets qui soient pour cet utilisateurs
    qs = queryset.filter(author=request.user)

    # on retourne la réponse d'une vue générique qui va nous faire le listing
    # de ces objets
    return object_list(request, queryset=qs, template_name=template_name)

    # En gros, l'auteur à CREER lui même sa propre vue générique
    # car je le rappelle, une vue générique est une vue normale (et une fonction
    # normale), mais qui a plein de paramètres pour la rendre souple et
    # réutilisable.
    # Sa vue générique à pour but de faire un listing d'objets appartenant
    # à l'utilisateur courant. Il délègue quand même le gros
    # du boulot à une vue générique de Django, parceque faut pas déconner, hein...

La vue peut être traduite ainsi en mode vue générique:

## views.py

class ListView(generic.ListView):
    queryset = MyObject.objects.all()
    template_name = "myproject/myobjects_list.html"

    def get_queryset(self):
        # c'est ici qu'on fait le filtre par l'utilisateur courant
        return self.queryset.filter(author=self.request.user)

# comme un décorateur ne fonctionne pas sur une classe, cette astuce
# permet de récupérer l'équivalent d'une vue wrappées et importable directement
# dans urls.py
listview = login_required(ListView.as_view())

Et c’est une bonne illustration de ce qui est relou avec les vues génériques:

class ListView(generic.ListView):
    queryset = MyObject.objects.all()
    template_name = "myproject/myobjects_list.html"
    def get_queryset(self):
        return self.queryset.filter(author=self.request.user)
listview = login_required(ListView.as_view())

VERSUS

@login_required
def listview(request, queryset=MyObject.objects.all(),
             template_name='myproject/myobject_list.html'):
    qs = queryset.filter(author=request.user)
    return object_list(request, queryset=qs, template_name=template_name)

Pour avoir les mêmes fonctionalités. Le bénéfice de l’un sur l’autre n’est pas énorme (il se trouve dans la réutilisabilité, dans des cas très poussés). Et il faut apprendre tout l’API des CBV pour faire la première.

Mais surtout, on perd le potentiel KISS dans le premier cas, car soyons franc, le plus souvent on a juste bsoin de ça:

@login_required
def listview(request):
    return object_list(request,
                       queryset=MyObject.objects.filter(author=request.user),
                       template_name='myproject/myobject_list.html')

Et ça, dans 3 mois, je le comprends tout de suite. Ca prends moins de place dans mon fichier. Et Max ne m’envoie pas de mails d’insultes.

Code 2

Routing:

## urls.py
urlpatterns = patterns('',
    (r'^list/', listview, {}, 'myobject_list'),
    # on rajoute une chtite vue pour créer une objet
    (r'^create/', createview, {}, 'myobject_create'),
)

Formulaire:

## forms.py
# on créé un formulaire à partir du model MyObject
# ce  formulaire permettra donc de créer une objet MyObject
class MyObjectForm(ModelForm):
    class Meta:
        model = MyObject
        exclude = ('author',)

    # le dev a ici choisi d'excluse le champ 'author' du formulaire
    # il veut en effet passer l'utilisateur en cours à la sauvegarde
    # afin que l'objet créé ait toujours pour autheur l'utilsateur courrant
    def save(self, user=None):
        # ici rien de fou, on fait un override du save, on appel le parent
        # et l'objet est créé. Amen.
        myobject = super(MyObjectForm, self).save(commit=False)
        myobject.author = user
        myobject.save()

Vue:

## views.py
from myproject.forms import MyObjectForm

# voici une vue normale, faite à la main
# elle elle réservée aux utilisateurs authentifiés
# mais accepte un paramètre pour lui dire sur quel formulaire travailler
# c'est donc encure une fois une vue générique faite à la mano
# puisque je vous le rappelle... Non, je déconne.
@login_required
def createview(request, form_class=MyObjectForm):

    # on check si la requête est une requête POST
    if request.method == 'POST':
        # si oui on prend les paramètres de la requête
        # et on les passe au formulaire
        # puis on vérifie si le formulaire est valide (pas d'erreurs de saisie)
        form = form_class(request.POST)
        if form.is_valid():
            # dans ce cas: on sauvegarde le formulaire en lui passant l'utilisateur
            # courant: un objet MyObjet est créé avec pour auteur
            # l'utilisateur courant
            myobject = form.save(user=request.user)
            # et on redirige sur la page décrivant l'objet
            # Ne cherchez pas comment il obtient ceci,
            # ce n'est pas expliqué dans son code surement volontairement
            # pour simplifier l'article, ce qui n'est pas plus mal
            return HttpResponseRedirect(myobject.get_absolute_url())

        # si le formulaire n'est pas valide, render_to_response
        # contiendra le formulaire avec les erreurs
    else:
        # si ce n'est pas une requête POST, on créé juste un formulaire
        # vierge à afficher
        form = form_class()

    # on retourne une réponse normale, avec le formulaire dans le context
    # et vous pouvez ignorer RequestContext, ça n'a pas d'importance pour nous
    # ici
    return render_to_response('myproject/myobject_form.html',
                              {'form': form},
                              context_instance=RequestContext(request))

Vous noterez que la vue précédent n’utilise pas de vue générique Django pour déléguer le boulot, ce qui explique qu’elle est longue.

Et voilà à quoi ressemblerait la vue en version CBV :

## views.py

from myproject.forms import MyObjectForm

class CreateView(generic.CreateView):
    form_class = MyObjectForm
    template_name = "myproject/myobject_form.html"

    # tous les classes génériques ont des hooks différent
    # ici on étend la CreateView, qui a a une méthode spécialement concue
    # pour la validation de formulaire
    # on l'override pour sauvegarder le formulaire en passant le user courant
    def form_valid(self, form):
        self.object = form.save(user=self.request.user)
        return super(CreateView, self).form_valid(form)

# idem que précédement
createview = login_required(CreateView.as_view())
]]>
http://sametmax.com/des-vues-normales-aux-vues-generiques-django/feed/ 13 1715