Mots-clé : python

Python : EAFP vs LBYL

Très souvent vous pouvez avoir deux styles de codes différents qui font la même chose en Python :


import os

if os.path.exists("fichier.txt"):
    os.unlink("fichier.txt")

import os
try:
    os.unlink("fichier.txt")
except OSError:  # levé si le fichier n'existe pas
    pass

Alors, lequel choisir ?

Personnellement, j’ai toujours préféré le premier choix, et pourtant… dans la documentation officielle, ils le déconseillent !

Pourquoi cela ? Explication : l’opposé de EAFP, c’est LBYL.

EAFP : Easier to ask for forgiveness than permission

Plus facile de demander pardon que la permission. Ce style de codage très utilisé en Python suppose l’existence de clés ou d’attributs valides et intercepte les exceptions si l’hypothèse s’avère fausse. Ce style propre et rapide se caractérise par la présence de nombreuses déclarations try and except. La technique contraste avec le style LBYL commun à de nombreux autres langages tels que C.

LBYL : Look before you leap

Réfléchir avant d’agir.

Ce style de codage teste explicitement les conditions préalables avant d’effectuer des appels ou des recherches. Ce style contraste avec l’approche EAFP et se caractérise par la présence de nombreuses déclarations if.

Dans un environnement multi-thread, l’approche LBYL peut risquer d’introduire une condition de concurrence entre « la vérification » et « la validation ». Par exemple, le code : if key in mapping: return mapping [key] peut échouer si un autre thread supprime la clé du mappage après le test, mais avant la recherche. Ce problème peut être résolu avec des verrous ou en utilisant l’approche EAFP.

Django : comment vérifier si on est en mode debug dans le template

Vous trouverez l’information dans beaucoup de sites, et cela semble très simple : dans votre template, il suffit de faire :

{% if not debug %}Je suis en debug !{% endif %}

En réalité cela ne suffit pas.

Il faut dans votre fichier settings.py, configurer correctement les adresses IP’s qui précisent qu’on est / ou pas / en mode « développement » :

INTERNAL_IPS = ['127.0.0.1', ]

(Notez que ce code peut être largement optimisé et dépendant de l’environnement, par exemple j’ai fait un settings.py qui prend cela en compte)

Django : faire des pages d’erreur sur mesure (404, 500, etc.)

Lorsque vous voulez faire des pages d’erreur sur mesure, c’est très simple… une fois que vous savez !
En effet, la documentation n’est pas très claire…

Voici un résumé de mon expérience, pour faire des pages d’erreur sur mesure (404, 500, etc.).
Il faut d’abord savoir que les erreurs sont des fonctions appelées (= vous ne pouvez pas le faire via les vues génériques).
Ensuite, la documentation donne un exemple, mais ce n’est pas assez.
Grâce aux raccourcis de Django, vous avez la fonction render() à laquelle vous pouvez passer un template et un contexte (= donc des variables).
Je me suis servi de cela pour créer un dictionnaire qui contient les erreurs, et les passer dans dans le contexte avec la clé title et content.


Voici le code des vues qui affiche une erreur « proprement » :

from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
VIEW_ERRORS = {
    404: {'title': _("404 - Page not found"),
          'content': _("A 404 Not found error indicates..."), },
    500: {'title': _("Internal error"),
          'content': _("A 500 Internal error means..."), },
    403: {'title': _("Permission denied"),
          'content': _("A 403 Forbidden error means ..."), },
    400: {'title': _("Bad request"),
          'content': _("A 400 Bad request error means ..."), }, }
def error_view_handler(request, exception, status):
    return render(request, template_name='errors.html', status=status,
                  context={'error': exception, 'status': status,
                           'title': VIEW_ERRORS[status]['title'],
                           'content': VIEW_ERRORS[status]['content']})
def error_404_view_handler(request, exception=None):
    return error_view_handler(request, exception, 404)
def error_500_view_handler(request, exception=None):
    return error_view_handler(request, exception, 500)
def error_403_view_handler(request, exception=None):
    return error_view_handler(request, exception, 403)
def error_400_view_handler(request, exception=None):
    return error_view_handler(request, exception, 400)


Une fois les vues faites, il faut aller dans la déclaration principale de vos vues. Donc le fichier urls.py qui est la racine de votre projet. Si vous mettez le code ailleurs, il sera ignoré.

Dedans, déclarez vos fonctions qui gèrent les erreurs :

handler404 = 'app.views.errors.error_404_view_handler'
handler500 = 'app.views.errors.error_500_view_handler'
handler403 = 'app.views.errors.error_403_view_handler'
handler400 = 'app.views.errors.error_400_view_handler'

Et enfin, dans vos templates, créez un fichier errors.html dans lequel vous pouvez construire votre page HTML qui gère l’erreur « proprement », avec, en plus dans le contexte, les variables {{ title }} et {{ content }} qui affichent respectivement le titre et le détail de l’erreur.

Maintenant, comment, en mode « développement », tester ces pages ? Dans la pratique vous ne pouvez pas, car en mode développement, une erreur affiche les informations de déboguage, et pas vos pages !
La solution : faire une URL qui « simule » l’erreur. Exemple avec 404 : vous ajouter dans vos urls : path('404/', error_404_view_handler), et puis d’afficher votre URL adéquate, soit http://localhost:8000/404/ et vous verrez l’erreur !

Django >= 2.1 : comment faire une administration « sur mesure » et « proprement »

Comment faire simplement une interface d’administration pour le projet entier

Sur les versions de Django < 2.1, il était impossible de surcharger « correctement » l’administration.

  • Créez un fichier admin.py à la racine de votre projet (« à la racine », parce qu’il est « global » à tout le projet)
  • Mettez-y ce code:
    from django.contrib import admin
    class MyProjectAdminSite(admin.AdminSite):
        title_header = 'My project Admin'
        site_header = 'My project administration'

    Bien sûr, changez « MyProject » par le nom de votre projet…
  • Dans settings.py, remplacez 'django.contrib.admin' par 'my_app.apps.MyAppAdminConfig',
  • Dans le fichier apps.py de votre projet, soit my_project/my_app/apps.py, déclarez l’administration comme suit :

    from django.apps import AppConfig
    from django.contrib.admin.apps import AdminConfig
    class MyAppConfig(AppConfig):
        name = 'my_app'
    class MyAppAdminConfig(AdminConfig):
        default_site = 'admin.MyProjectAdminSite'

Quand utiliser __repr__ ou __str__ ?

Cette astuce vient de Dan Bader. Du code, rapidement :

# Émuler ce que fait la std lib :
>>> import datetime
>>> aujourdhui = datetime.date.today()
# Le résultat de __str__ doit être lisible = comme une chaîne :
>>> str(today)
'2017-02-02'
# Le résultat de __repr__ doit lever les ambiguïtés possibles :
>>> repr(today)
'datetime.date(2017, 2, 2)'
# L'interpréteur Python en ligne de commande
# utilise __repr__ pour inspecter les objets :
>>> today
datetime.date(2017, 2, 2)

Django : le polymorphisme : astuces

Un excellent article écrit par Dan Bader (j’ai pas mal discuté avec lui et j’ai son livre, il est vraiment aussi bon que modeste, je vous recommande vivement tous ses articles – et n’hésitez pas à lui parler en Anglais (tant qu’il n’est pas encore trop connu !), il vous répondra sûrement).

Il manque juste ces « petits » détails techniques lorsqu’on se base sur les modèles abstraits et l’héritage au sens « modèle » de Django (Abstract Base Model).

Prenons un exemple simple : vous créez un modèle « simple » Media(models.Model) dont vous allez hériter sur deux modèles : Book(Media) et Dvd(Media).

Ce qui va vous prendre beaucoup de temps (et qui n’est expliqué nulle part de manière claire et directe), c’est que Django, de manière cachée, va créer des propriétés qui vont dans les deux sens entre le modèle parent et le modèle enfant.

Pour reprendre notre exemple, avec les trois modèles : Media = parent, et les deux enfants Book et Dvd.
Imaginez que pour chaque enfant, vous créiez une méthode spécifique, par exemple Book.nb_pages() et Dvd.is_blue_ray()
De manière cachée, vous pourrez à partir du parent, accéder à l’enfant et ses propriété via son nom comme ceci :
my_media = Media.object.get(pk=1)
if hasattr(self, 'book'):  # this Media is a Book:
    my_media.book.nb_pages()
elif hasattr(self, 'dvd'):  # this media is a Dvd:
    my_media.dvd.is_blue_ray()
else:  # direct Media model access:
    pass

A l’inverse, à partir des modèles enfants, pour accéder au parent c’est le nom du modèle parent avec _ptr :
my_book = Book.object.get(pk=21)
my_dvd = Dvd.object.get(pk=75)
# let's imagine "price" is a Media property. You can do this:
print(my_book.media_ptr.price)
print(my_dvd.media_ptr.price)

Django : favicon.ico : comment le gérer facilement

En mode débug, un serveur Django renvoie tous les fichiers, y compris les fichiers statique. Seul problème : tous les fichiers statique on leur URL qui commence par /static/.

Mais on rencontre un problème : le favicon.ico n’est pas un fichier statique « normal » = qui commence par /static/. C’est un nom « en dur », qui ressemble à http://monsite/favicon.ico.

Il vous faut donc la coder en dur dans les URLs : ainsi : url(r'^favicon.ico/$',...)
Et pour renvoyer directement le contenu, il suffit de faire une fonction immédiate (= lambda) qui renvoie directement le contenu :
urlpatterns = [
    # ...blabla... all path and now:
    url(r'^public/(?P.*)$', static.serve, {
        'document_root': settings.MEDIA_ROOT
    }, name='url_public'),
    url(r'^favicon.ico/$', # google chrome favicon fix :
        lambda x: HttpResponseRedirect(settings.STATIC_URL+'favicon.ico')),

]

Django : comment passer une constante à tous vos templates ?

Django, nativement, a la possibilité de passer des dictionnaires clé-valeurs à tous les templates, et dans les templates on accède aux valeurs via la clé.

Exemple concret : je voulais passer le nom de mon site à tous les templates, mais sous forme de constante.

Dans settings.py j’ai défini ma constante : WEBSITE_NAME = 'mywebsite'

Il suffit de créer une fonction qui renvoie un dictionnaire avec une clé nommée « correctement », par exemple :

def context_processor_website_name(request):
    return {'website_name': WEBSITE_NAME}

Et ensuite, dans les context_processors, juste ajouter le nom de la fonction par rapport au package où elle est.
J’ai mis cette fonction dans settings.py.
Je ne sais pas si c’est la meilleure place mais l’idée c’est que comme c’est en rapport avec la configuration de mon site, c’est le meilleur endroit…

Donc pour préciser où est la fonction c’est 'myproject.settings.context_processor_website_name' :

On retrouve donc le code final comme ceci :

TEMPLATES = [{
    'BACKEND': 'django.template.backends.django.DjangoTemplates',
    'DIRS': [os.path.join(BASE_DIR, 'templates')],
    'APP_DIRS': True,
    'OPTIONS': {
        'context_processors': [
            'django.template.context_processors.debug',
            # ... and:
            'myproject.settings.context_processor_website_name'
    ],
    },
}, ]

Et voilà, à partir de maintenant, avec exactement 3 lignes de code, j’ai accès à ma variable website_name, que je peux utiliser ainsi :

<title>{% block title %}{% if title %}{{ title|safe }} - {% endif %}{{ website_name }}{% endblock %}</title>

Petite parenthèse : grâce à ce code, si la page qu’on affiche a un titre, elle aura pour titre : [titre] - [nom du site] et si elle n’a aucun titre, elle affichera : [nom du site]

Django : un modèle sur mesure : DaysField code source complet

Code source complet de DaysOfWeek

Après avoir écrit en trois parties (1, 2 et 3) comment faire un modèle sur mesure, voici le code source au complet.

from django.core.exceptions import ValidationError
from django import forms
from django.db import models
from django.utils.translation import ugettext_lazy as _
class DaysOfWeek:
    DAYS = {1: _("Monday"),
            2: _("Tuesday"),
            3: _("Wednesday"),
            4: _("Thursday"),
            5: _("Friday"),
            6: _("Saturday"),
            7: _("Sunday"), }
    DAYS_SHORT = {1: _("Mo"),
                  2: _("Tu"),
                  3: _("We"),
                  4: _("Th"),
                  5: _("Fr"),
                  6: _("Sa"),
                  7: _("Su"), }
    CHOICES = [(idx, value) for idx, value in DAYS.items()]
    @staticmethod
    def summary_from_list(tab, empty=' '):
        if tab is None:
            return '-'.join([empty for a in range(len(DaysOfWeek.DAYS))])
        return '-'.join([str(DaysOfWeek.DAYS_SHORT.get(i, empty)) for i in tab])
class DaysFormField(forms.TypedMultipleChoiceField):
    # different widget, comment to change interface:
    widget = forms.CheckboxSelectMultiple
    def __init__(self, *args, **kwargs):
        if 'max_length' in kwargs:
            kwargs.pop('max_length')
        kwargs['choices'] = DaysOfWeek.CHOICES
        super().__init__(*args, **kwargs)
class DaysField(models.CharField):
    description = _("Comma-separated integers between 1 and 7")
    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 13 # max. len = all days = "1,2,3,4,5,6,7" = 13
        super().__init__(*args, **kwargs)
    @staticmethod
    def value_to_array(value):
        if value is None:
            return None
        try:
            if isinstance(value, list):
                return [int(a) for a in value]
            elif isinstance(value, str):
                return [int(a) for a in value.split(',')]
        except (TypeError, ValueError):
            raise ValidationError(_("Unexpected value"))
        raise ValidationError(_("Unexpected value"))
    @staticmethod
    def from_db_value(value, expression, connection):
        return DaysField.value_to_array(value)
    def to_python(self, value):
        return DaysField.value_to_array(value)
    def get_prep_value(self, value):
        return ','.join([str(a) for a in value]) if value is not None else None
    def formfield(self, **kwargs):
        # ignore the admin directives: directly override with our custom form:
        return super().formfield(form_class=DaysFormField,
                                 initial=[])
    # region - Validator -
    class ListBetween1And7Validator:
        def __call__(self, value):
            try:
                if isinstance(value, list):
                    loop = [int(a) for a in value]
                elif isinstance(value, str):
                    loop = [int(a) for a in value.strip('[]').split(',')]
                else:
                    raise ValueError()
                for v in loop:
                    if not (7 >= v >= 1):
                        raise ValueError()
            except (TypeError, ValueError):
                raise ValidationError(
                    _("Enter a list if coma-separated values between 1 and 7."),
                    code='invalid')
    # endregion - Validator -
    default_validators = [ListBetween1And7Validator()]