La toute première version de Python 3.6 avait un bug assez vicieux qui ne se manifestait que sous certaines conditions, généralement dans un daemon sur un serveur, et en important certains modules qui finissent pas déclencher par réaction en chaîne l’usage de random.
django est concerné.
On tombait dessus généralement assez tard, à la mise en prod, avec un message cryptique:
BlockingIOError: [Errno 11] Resource temporarily unavailable
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
.... <- des imports de votre code qui ne font rien de mal
File "/usr/local/lib/python3.6/site-packages/pkg_resources/__init__.py", line 36, in
import email.parser
File "/usr/local/lib/python3.6/email/parser.py", line 12, in
from email.feedparser import FeedParser, BytesFeedParser
File "/usr/local/lib/python3.6/email/feedparser.py", line 27, in
from email._policybase import compat32
File "/usr/local/lib/python3.6/email/_policybase.py", line 9, in
from email.utils import _has_surrogates
File "/usr/local/lib/python3.6/email/utils.py", line 28, in
import random
File "/usr/local/lib/python3.6/random.py", line 742, in
_inst = Random()
SystemError: returned a result with an error set
Cela a été corrigé rapidement, et le binaire patché ajoute juste un “+” à sa version:
$ python --version
Python 3.6.0+
En théorie vous ne pouvez pas tomber dessus, tous les liens de téléchargement ont été mis à jour, les distributions ont changé leurs dépôts, etc.
Mais hier je me suis fait bien niqué, et j’ai perdu 1h à debugguer cette surprise qui n’avait aucun sens (puisque mon code allait bien) : les bugs dans les binaires officiels sont rares et c’est le dernier endroit où je cherche.
En effet, certaines sources non-officielles pour installer Python n’ont pas été mise à jour, et c’est le cas du très populaire PPA deadsnake.
Donc si vous avez compilé Python à la main ou utilisé un PPA, assurez-vous bien d’avoir la bonne version, et sinon upgradez. En attendant j’ai un bug report à faire à deadsnakes…
On ne veut pas mettre sa SECRET_KEY en prod, et utiliser un service pour générer la clé, ça va deux minutes.
Générer une clé secrète:
importrandomimportstringdef secret_key(size=50):
pool =string.ascii_letters + string.digits + string.punctuationreturn"".join(random.SystemRandom().choice(pool)for i inrange(size))
Générer une clé secrete avec une commande manage.py:
from django.core.management.baseimport BaseCommand, CommandError
from polls.modelsimport Question as Poll
class Command(BaseCommand):
help='Generate a secret key'def add_arguments(self,parser):
parser.add_argument('size', default=50,type=int)def handle(self, *args, **options):
self.stdout.write(secret_key(options['size']))
A mettre dans ./votreapp/management/command/generate_secret_key.py :)
Une fonction pour lire la clé depuis un fichier texte ou générer la clé si elle n’existe pas:
import io
importostry:
importpwdexceptImportError:
passtry:
importgrpexceptImportError:
passdef secret_key_from_file(
file_path,
create=True,
size=50,
file_perms=None,# unix uniquement
file_user=None,# unix uniquement
file_group=None# unix uniquement):
try:
with io.open(file_path)as f:
return f.read().strip()exceptIOErroras e:
if e.errno==2and create:
with io.open(file_path,'w')as f:
key = secret_key(size)
f.write(key)ifany((file_perms, file_user, file_group))andnotpwd:
raiseValueError('File chmod and chown are for Unix only')if file_user:
os.chown(file_path, uid=pwd.getpwnam(file_user).pw_uid)if file_group:
os.chown(file_path, gid=grp.getgrnam(file_group).gr_gid)if file_perms:
os.chmod(file_path,int(str(file_perms),8))return key
raise
Et une fonction pour récupére la clé depuis une variable d’environnement ou un fichier:
Si il y a bien une mailing-list à suivre, c’est Python-dev. Elle regorge de tout, on est y apprend sans cesse à propos de Python, la programmation en général, la gestion de communautés, etc. Mais c’est accessible pour peu qu’on soit à l’aise en anglais.
Parmi les sujets chauds du moment, il y a l’introduction, pour potentiellement Python 3.7, d’un mot clé pour évaluer paresseusement les expressions.
Je m’explique…
On a déjà plusieurs moyens de faire du lazy loading en python :
Le shortcut des opérateurs and et or. La partie droite ne s’exécute que si la gauche ne répond pas déjà à la question.
Les générateurs. Il ne sont évalués qu’au premier appel de next().
await : évalué seulement quand la boucle d’événement décide de passer par là.
La nouvelle proposition est quelque chose de différent : permettre de déclarer une expression arbitraire, mais qui n’est évaluée que la première fois qu’on la lit.
Ca ressemble à ça:
def somme(a, b):
print('coucou')return a + b
truc = lazy somme(a, b)print("Hello")print(truc)
Ce qui afficherait:
Hello
coucou
3
On peut mettre ce qu’on veut après le mot clé lazy. Le code n’est exécuté qu’une fois qu’on essaye d’utiliser la variable truc.
L’usage essentiel, c’est de pouvoir déclarer du code sans chichi, comme si on allait l’utiliser maintenant. Le passer à du code qui va l’utiliser sans même avoir besoin de savoir que c’est un truc spécial. Et que tout marche à la dernière minute naturellement.
Par exemple, la traduction d’un texte dans un code Python ressemble souvent à ça :
fromgettextimportgettextas _
...
print(_('Thing to translate'))
Mais dans Django on déclare un champ de modèle dont on veut pouvoir traduire le nom comme ceci :
from django.utils.translationimport ugettext_lazy
class Produit(models.Model):
...
price= models.IntegerField(verbose_name=ugettext_lazy("price"))
La raison est qu’on déclare ce texte au démarrage du serveur, et on ne sait pas encore la langue dans laquelle on va le traduire. Cette information n’arrive que bien plus tard, quand un utilisateur arrive sur le site. Mais pour détecter toutes les chaînes à traduire, créer le fichier de traduction, construire le cache, etc., il faut pouvoir marquer la chaîne comme traductible à l’avance.
Django a donc codé ugettext_lazy et tout un procédé pour évaluer cette traduction uniquement quand une requête Web arrive et qu’on sait la langue de l’utilisateur.
Avec la nouvelle fonctionnalité, on pourrait juste faire:
fromgettextimportgettextas _
class Produit(models.Model):
...
price= models.IntegerField(verbose_name=lazy _("price"))
Rien à coder nul part du côté de Django, rien à savoir de plus pour un utilisateur. Ça marche dans tous les cas pareil, pour tout le monde, dans tous les programmes Python.
Bref, j’aime beaucoup cette idée qui permet de s’affranchir de pas mal de wrappers pour plein de trucs, mais aussi beaucoup aider les débutants. En effet les nouveaux en programmations font généralement des architectures basiques : pas d’injection de dépendances, pas de factories, etc. Avec lazy, même si une fonction n’accepte pas une factory, on peut quand même passer quelque chose qui sera exécuté plus tard.
Évidement ça ne dispense pas les gens de faire ça intelligemment et d’attendre des callables en paramètre. Dans le cas de Django, une meilleure architecture accepterait un callable pour verbose_name par exemple.
Mais c’est un bon palliatif dans plein de situations. Et l’avantage indiscutable, c’est que le code qui utilise la valeur paresseuse n’a pas besoin de savoir qu’elle le fait.
Les participants sont assez enthousiastes, et moi aussi, bien que tout le monde a conscience que ça pose plein de questions sur la gestion des générateurs, de locals(), et du debugging en général.
Plusieurs mots clés sont actuellement en compétition: delayed, defer, lazy. delayed est le plus utilisé, mais j’ai un penchant pour lazy.
Ça faisait un bail que j’avais pas parlé d’un don du mois. Le don du mois n’est pas un don mensuel, mais un don que je fais pour le mois. Des fois j’oublie. Des fois je n’ai pas de thune. Des fois je ne me sens pas généreux, que l’humanité aille crever dans sa crasse !
Mais quand les astres du pognon et de la bonne humeur sont alignés je m’y remets.
J’ai pris des nouvelles de nuitka, un outil qui permet de compiler du code Python en un exe indépendant. Malgré le fait que l’auteur soit visiblement le seul à vraiment travailler dessus, le projet continue d’avancer avec régularité et détermination. Corrections de bugs, optimisation (amélioration de la comptabilité (la 3.5 est supportée, la 3.6 en cours !)).
J’ai été agréablement surpris de voir que l’outil s’était encore amélioré. Le hello world stand alone m’a pris quelques minutes à mettre en œuvre, d’autant que nuitka est dans les dépôts Ubuntu donc l’installation ne demande aucun effort.
Comme pouvoir shipper du code Python sans demander à l’utilisateur final d’installer Python est quelque chose qui est en forte demande en ce moment, j’ai voulu soutenir le projet, et j’ai fait un don de 50 euros.
Puis je me suis souvenu qu’en fait, j’en avais déjà fait un l’année dernière :) Bah, l’auteur mérite qu’on le soutienne. Des mecs comme ça y en a pas des masses.
Je lis régulièrement des commentaires de batailles d’opinions sur le PEP8. Pas mal sont en fait dues à un manque de compréhension de son application. Je vais donc vous montrer des codes et les transformer pour qu’ils soient plus propre.
Rappelez-vous toujours que des recommandations stylistiques sont essentiellement arbitraires. Elles servent à avoir une base commune, et il n’y en pas de meilleures.
On recommande les espaces au lieu des tabs. Les tabs sont sémantiquement plus adaptés et plus flexibles. Les espaces permettent d’avoir un seul type de caractères non imprimables dans son code et autorise un alignement fin d’une expression divisée en plusieurs lignes. Il n’y a pas de meilleur choix. Juste un choix tranché pour passer à autre chose de plus productif, comme coder.
On recommande par contre une limite de 80 caractères pour les lignes. Cela permet à l’œil, qui scanne par micro-sauts, de parser plus facilement le code. Mais aussi de faciliter le multi-fenêtrage. Néanmoins cette limite peut être brisée ponctuellement si le coût pour la lisibilité du code est trop important. Tout est une question d’équilibre.
Ready ? Oh yeah !
L’espacement
def foo (bar ='test'):
if bar=='test' : # this is a test
bar={1:2,3 : 4*4}
Devient:
def foo(bar='test'):
if bar =='test': # this is a test
bar ={1: 2,3: 4*4}
On ne double pas les espaces. On a pas d’espace avant le ‘:’ ou le ‘,’ mais un après. Les opérateurs sont entourés d’espaces, sauf le ‘=’ quand il est utilisé dans la signature de la fonction ou pour les opérations mathématiques (pour cette dernière, les deux sont tolérés).
Un commentaire inline est précédé de 2 espaces, une indentation est 4 espaces.
Les sauts de lignes aussi sont important:
import stuff
def foo():
passdef bar():
pass
Devient:
import stuff
def foo():
passdef bar():
pass
Les déclaratitons à la racine du fichiers sont séparés de 2 lignes. Pas plus, pas moins.
Mais à l’intérieur d’une classe, on sépare les méthodes d’une seule ligne.
class Foo:
def bar1():
passdef bar2():
pass
Devient:
class Foo:
def bar1():
passdef bar2():
pass
Tout cela contribue à donner un rythme régulier et familier au code. Le cerveau humain adore les motifs, et une fois qu’il est ancré, il est facile à reconnaître et demande peu d’effort à traiter.
Les espaces servent aussi à grouper les choses qui sont liés, et a séparer les choses qui ne le sont pas.
Le style de saut de ligne à l’intérieur d’une fonction ou méthode est libre, mais évitez de sauter deux lignes d’un coup.
def lower_case_lol_ptdr():
...
class UpperCaseLOLPTDR:
...
Malgré la possibilité d’utiliser des caractères non ASCII dans les noms en Python 3, ce n’est pas recommandé. Même si c’est tentant de faire:
>>> Σ =lambda *x: sum(x)>>> Σ(1,2,3)6
Néanmoins c’est l’arbre qui cache la foret. Il y a plus important : donner un contexte à son code.
Si on a:
numbers =(random.randint(100)for _ inrange(100))
group =lambda x: sum(map(int,str(x)))
numbers =(math.sqrt(x)for x in numbers if group(x)==9)
Le contexte du code donne peu d’informations et il faut lire toutes les instructions pour bien comprendre ce qui se passe. On peut faire mieux:
def digits_sum(number):
""" Take a number xyz and return x + y + z """returnsum(map(int,str(number)))
rand_sample =(random.randint(100)for _ inrange(100))
sqrt_sample =(math.sqrt(x)for x in rand_sample if digits_sum(x)==9)
Ici, on utilisant un meilleur nommage et en rajoutant du contexte, on rend son code bien plus facile à lire, même si il est plus long.
Le PEP8 n’est donc que le début. Un bon code est un code qui s’auto-documente.
Notez que certains variables sont longues, et d’autres n’ont qu’une seule lettre. C’est parce qu’il existe une sorte de convention informelle sur certains noms dans la communauté :
i et j sont utilisés dans pour tout ce qui est incrément:
for i, stuff inenumerate(foo):
x, y et z sont utilisés pour contenir les éléments des boucles:
for x in foo:
for y in bar:
_ est utilisé soit comme alias de gettext:
fromgettextimportgettextas _
Soit comme variable inutilisée:
(random.randint(100)for _ inrange(100))
_ a aussi un sens particulier dans le shell Python : elle contient la dernière chose affichée automatiquement.
f pour un fichier dans un bloc with:
withopen(stuff)as f:
Si vous avez deux fichiers, nommez les:
withopen(foo)as foo_file,open(bar)as bar_file:
*args et **kwargs pour toute collection d’arguments hétérogène :
def foo(a, b, **kwarg):
Mais attention, si les arguments sont homogènes, on les nomme:
def merge_files(*paths):
Et bien entendu self pour l’instance en cours, ou cls pour la classe en cours:
class Foo:
def bar(self):
...
@classmethoddef barbar(cls):
...
Dans tous les cas, évitez les noms qui sont utilisés par les built-ins :
Les parenthèses permettent des choses merveilleuses
query = db.query(MyTableName).filter_by(MyTableName.the_column_name== the_variable, MyTableName.second_attribute> other_stuff).first())string="Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
query =(db.query(MyTableName)
.filter_by(MyTableName.the_column_name== the_variable,
MyTableName.second_attribute> other_stuff)
.first())string=("Lorem ipsum dolor sit amet, consectetur adipisicing elit, ""sed do eiusmod tempor incididunt ut labore et dolore magna ""aliqua. Ut enim ad minim veniam, quis nostrud exercitation ""ullamco laboris nisi ut aliquip ex ea commodo consequat.")
Les variables intermédiaires documentent le code:
sqrt_sample =(math.sqrt(x)for x in(random.randint(100)for _ inrange(100))ifsum(map(int,str(number)))==9)
Devient bien plus clair avec :
def digits_sum(number):
""" Take a number xyz and return x + y + z """returnsum(map(int,str(number)))
rand_sample =(random.randint(100)for _ inrange(100))
sqrt_sample =(math.sqrt(x)for x in rand_sample if digits_sum(x)==9)
return, break et continue permettent de limiter l’indentation:
def foo():
if bar:
rand_sample =(random.randint(100)for _ inrange(100))return(math.sqrt(x)for x in rand_sample if digits_sum(x)==9)returnNone
Est plus élégant écrit ainsi:
def foo():
ifnot bar:
returnNone
rand_sample =(random.randint(100)for _ inrange(100))return(math.sqrt(x)for x in rand_sample if digits_sum(x)==9)
any, all et itertools.product évident pas mal de blocs:
for x in foo:
if barbarbarbarbarbarbar(x):
meh()for x in foo:
for y in bar:
meh(y, y)
Devient:
is_bar =(barbarbarbarbarbarbar(x)for x in foo)ifany(is_bar):
meh()importitertoolsfor x, y initertools.product(foo, bar):
meh(y, y)
Le fait est que Python est bourré d’outils très expressifs. Oui, il arrivera parfois que vous deviez briser la limite des 80 charactères. Je le fais souvent pour mettre des URLs en commentaire par exemple. Mais ce n’est pas le cas typique si vous utilisez le langage tel qu’il a été prévu.
Passer en mode pro
Le PEP8, c’est un point de départ. Quand on a un script et qu’il grandit sous la forme d’une bibliothèque, il faut plus que le reformatter.
A partir d’un certain point, on voudra coiffer son code et lui mettre un costume. Reprenons :
def digits_sum(number):
""" Take a number xyz and return x + y + z """returnsum(map(int,str(number)))
rand_sample =(random.randint(100)for _ inrange(100))
sqrt_sample =(math.sqrt(x)for x in rand_sample if digits_sum(x)==9)
On lui fait un relooking pour son entretient d’embauche :
def digits_sum(number):
""" Take a number xyz and return x + y + z
Arguments:
number (int): the number with digits to sum.
It can't be a float.abs
Returns:
An int, the sum of all digits of the number.
Example:
>>> digits_sum(123)
6
"""returnsum(map(int,str(number)))def gen_squareroot_sample(size=100, randstart=0, randstop=100, filter_on=9):
""" Generate a sample of random numbers square root
Take `size` number between `randstart` and `randstop`,
sum it's digits. If the resulting value is equal to `filter_on`,
yield it.
Arguments:
size (int): the size of the pool to draw the numbers from
randstart (int): the lower boundary to generate a number
randstop (int): the upper boundary to generate a number
filter_on (int): the value to compare to the digits_sum
Returns:
Generator[float]
Example:
>>> list(gen_squareroot_sample(10, 0, 100, filter_on=5))
[5.291502622129181, 6.708203932499369, 7.280109889280518]
"""for x inrange(size):
dsum = digits_sum(random.randint(randstart, randstop))if dsum == filter_on:
yieldmath.sqrt(x)
Et dans un autre fichier :
sqrt_sample = gen_squareroot_sample()
L’important ici:
Le code devient modulable car on en fait des fonctions
Les fonctions ont des paramètres avec des valeurs par défaut pour faciliter la vie.
On utilise les docstrings pour documenter le code.
Des noms bien choisis complètent cette documentation. Le code devient aussi plus verbeux pour laisser la place à la modularité, mais aussi la facilité d’édition.
Il ne faut pas laisser le PEP8 vous limiter à la vision de la syntaxe. La qualité du code est importante : sa capacité à être lu, compris, utilisé facilement et modifié.
Pour cette même raison, il faut travailler ses APIS.
Le style d’un langage va bien au-delà des règles de la syntaxe.
Les docstrings
Je suis beaucoup plus relaxe sur le style des docstrings. Il y a pourtant bien le PEP 257 pour elles.
Simplement ne pas le respecter parfaitement n’affecte pas autant leur lisibilité car c’est du texte libre.
Quelques conseils tout de même.
Si elle peut tenir sur une ligne, mettez tout sur une ligne, y compris les """.
Utilisez """, pas '''. La majorité des gens le font, et ça fera très bizarre de mélanger.
Choisissez une convention de documentation et tenez-vous y. Les plus populaires sont celles de sphinx, numpy et Google. J’ai une préférence pour la dernière.
N’hésitez pas à la faire longue, avec un example. Il n’est pas rare que la docstring soit plus longue que le code quelle document.
Une bonne docstring et une fonction gardée courte et avec une signature bien nommée a rarement besoin de commentaires.
Les docstests sont un enfer à maintenir.
En bonus
flake8 est un excellent linter qui vérifiera votre style de code. Il existe en ligne de commande ou en plugin pour la plupart des éditeurs.
Dans le même genre, mccabe vérifira la complexité de votre code et vous dira si vous êtes en train de fumer en vous attributant un score. Il existe aussi intégré comme plugin de flake8 et activable via une option.
Tox vous permet d’orchestrer tout ça, en plus de vos tests unittaires. Je ferai un article dessus un de ces 4.
Si vous voyez des commentaires comme # noqa ou # xxx: ignore ou # xxx: disable=YYY, ce sont des commentaires pour pontuellement dire à ces outils de ne pas prendre en considération ces lignes.
Car souvenez-vous, ces règles sont là pour vous aider. Si à un moment précis elles cessent d’etre utiles, vous avez tous les droits de pouvoir les ignorer.
Mais ces règles communes font de Python un langage à l’écosystème exceptionnel. Elles facilitent énormément le travail en équipe, le partage et la productivité. Une fois habitué à cela, travailler dans d’autres conditions vous paraitra un poids inutile.