Ah, l’encoding, le truc que tout le monde veut mettre sous le tapis. Il faut dire que c’est dur à gérer. En fait tellement dur que:
Les logiciels et pages web continuent parfois d’afficher des ?? en 2017. Tous les langages laxistes laissent des données corrompues plutôt que d’avertir le codeur qui du coup n’apprend jamais à faire les choses correctement.
PHP a littéralement abandonné la version 6 car impossible de trouver une solution propre avec leur design.
NodeJS ignore juste la question, fout tout en utf8 et vous dit de télécharger une lib externe si vous voulez gérer autre chose.
Tout ce bordel amène les devs à essayer d’ignorer le problème le plus longtemps possible. Ca marche assez bien pour les anglophones car leur environnement est assez homogène, orienté ASCII, et certains peuvent faire une très belle carrière en restant joyeusement ignorant.
Ca marche beaucoup moins bien pour les européens, et pas du tout pour le monde arabe et asiatique. Néanmoins, pas besoin de chercher bien loin pour trouver des échecs critiques.
Naviguez tranquillement sur un site espagnol a priori joli, moderne, utilisant des tildes et tout ce qu’il faut. Maintenant regardez la requête HTTP, vous noterez que le serveur n’indique pas le charset du contenu. Fort heureusement dans le HTML vous trouvez:
Nickel, récupérons le texte du bouton “Ver más ideas”:
>>>import requests
>>> res = requests.get('http://www.airedefiesta.com/76-pinatas-y-chuches.html')>>> data = res.content.split(b'http://www.airedefiesta.com/ideas.html?c=76">')[1].split(b'</a>')[0]>>> data
b'Ver m\xc3\xa1s ideas'
Une suite de bits comme maman les aime. On décode:
>>> data.decode('ISO-8859-1')'Ver más ideas'
Et puis on ne voudrait pas que vous arriviez au château trop vite
Enfer et sodomie ! Le charset déclaré n’est pas celui utilisé. Tentons un truc au hasard:
>>> data.decode('utf8')'Ver más ideas'
Bref, en 2017, on se touche la nouille pour savoir qui a son architecture multi-services load balancée web scale à base de NoSQL, de containers orchestrés et de serveurs asynchrones. Mais pour afficher du texte y a plus personne hein…
Vous croyez que ce ne sont que les amateurs qui font ces erreurs. Naaaaaaaaa. Par exemple le standard pour les fichiers zip a une vision très… hum… personnelle du traitement de l’encoding des noms de fichier.
L’encoding, c’est la raison majeur de l’incompatibilité de Python 2 et 3, mais aussi un signe de la bonne santé de la techno puisque c’est un des rares vieux langages (je rappelle que Python est plus vieux que Java) à gérer la chose correctement. A savoir:
Avoir un type haut niveau qui fait abstraction de l’encoding pour le texte.
Forcer le développeur à spécifier l’encoding pour les entrées et les sorties.
Eviter toute conversion automatique.
Avoir de l’utf8 par défaut là où ça a du sens.
Lever des erreurs plutôt que de corrompre les données.
Avoir une API unifiée autour de la notion de “codec” qui marche pour le FS, le réseau, les chaînes internes, etc.
Python n’est pas parfait pour autant. Par exemple il garantit un accès 0(1) indexing sur les strings, ce qui à mon sens est inutile. Swift a un meilleur design pour ce genre de choses. Mais globalement c’est quand même super bon.
Si ne savez toujours pas comment ça marche, on a évidement un tuto pour ça.
Alors pourquoi l’encoding c’est un truc compliqué ?
Et bien parce que comme pour le temps ou l’i18n, ça touche à la culture, au langage, à la politique, et on a accumulé les problèmes au fil des années.
Mais je vous jure ça avait du sens y a 40 ans !
Par exemple, parlons un peu d’UTF.
Vous savez, on vous dit toujours d’utiliser utf8 partout pour être tranquille…
Mais déjà se pose la question : avec ou sans BOM ?
Le BOM, c’est une suite d’octets qui indique en début de fichier qu’il contient de l’UTF. Si ça à l’air pratique, c’est parce que ça l’est. Malheureusement, celui-ci n’est pas obligatoire, certaines applications le requièrent, d’autres l’ignorent, et d’autres plantent face au BOM. D’ailleurs, le standard unicode lui-même ne le recommande pas:
Use of a BOM is neither required nor recommended for UTF-8
Ca aide vachement à faire son choix.
Perso je ne le mets jamais, sauf si je dois mélanger des fichiers de différents encodings et les différencier plus tard.
Mais Powershell et Excel par exemple, fonctionnent parfois mieux si vous leur passez des données avec le BOM :)
Si vous avez un peu creusé la question, vous savez qu’il existe aussi UTF16 (par défaut dans l’API de Windows 7 et 8 et les chaînes de .NET), UTF32 et UTF64. Ils ont des variantes Big et Little Endians, qui ne supportent pas le BOM, et une version neutre qui le supporte, pour faciliter la chose.
Bien, bien, bien.
Mais saviez-vous qu’il existe aussi UTF-1, 5 et 6 ? Si, si. Et UTF9 et UTF18 aussi, mais sauf que eux ce sont des poissons d’avril, parce que les gens qui écrivent les RFC sont des mecs trop funs en soirées.
Que sont devenus ces derniers ? Et bien ils ont été proposés comme encoding pour l’internationalisation des noms de domaine. UTF5 est un encoding en base 32, comme son nom l’indique. Si, 2 puissance 5 ça fait 32. Funs en soirée, tout ça.
Néanmoins quelqu’un est arrivé avec une plus grosse bit(e), punycode, en base 36, et a gagné la partie. J’imagine que les gens se sont dit qu’utiliser base64 était déjà trop fait par tout le monde est qu’on allait pas se priver de cette occasion fabuleuse de rajouter un standard.
Standard qui ne vous dispense pas, dans les URLs, d’encoder DIFFÉREMMENT ce qui n’est pas le nom de domaine avec les bons escaping. Et son lot de trucs fantastiques. Encoding qui est différent pour les valeurs de formulaire.
En plus, si Punycode est l’encoding par défaut utilisé dans les noms de domaine, c’est donc aussi celui des adresses email. Ce qui vous permettra de profiter des interprétations diverses de la spec, comme par exemple le retour de la valeur d’un HTML input marqué “email”, qui diffère selon les navigateurs.
If you don’t encode in Tarrlytons…fuck you!
Pourquoi je vous parle des adresses emails tout à coup ? Ah ah ah ah ah ah ah !
Mes pauvres amis.
Je ne vous avais jamais parlé d’utf7 ?
Non, je ne me fous pas de votre gueule. Je suis très sérieux.
UTF7 est en effet l’encoding par défaut pour IMAP, particulièrement les noms des boîtes aux lettres. Vous savez, “INBOX”, “Spams” et “Messages envoy&AOk-s” ;)
Or comme l’enculerie ne serait pas aussi délicieuse sans un peu de sable…
La version utilisée maintenant (et pour toujourssssssss) par IMAP est une version d’UTF7 non standard et modifiée.
Pourquoi ? Ben parce qu’allez-vous faire foutre.
The choosen one would soon realize that some things survived outside of the vault. Like bad UI and terrible IT standards. And his ‘science’ skills is at 42% and life sucks.
Au final je n’ai fait que parloter d’UTF, mais souvenez-vous que:
Donc on n’a fait qu’effleurer la surface de l’anus boursouflé de la mouche.
J’espère que la nuit, à 3h du mat, lorsque votre prochaine mise en prod agonisera sur un UnicodeDecodeError, vous penserez à moi et pendant un instant, un sourire se dessinera sous vos larmes.
Avec de nombreuses distros Linux qui viennent avec Python 3 par défaut ainsi que Django et Pyramid qui annoncent bientôt ne plus supporter Python 2, il est temps de faire un point.
Python 3 est aujourd’hui majoritairement utilisé pour tout nouveau projet ou formation que j’ai pu rencontrer. Les plus importantes dépendances ont été portées ou possèdent une alternative. Six et Python-future permettent d’écrire facilement un code compatible avec les deux versions dans le pire des cas.
Nous sommes donc bien arrivés à destination. Il reste quelques bases de code encore coincées, la vie est injuste, mais globalement on est enfin au bout de la migration. Mais ça en a mis du temps !
Il y a de nombreuses raisons qui ont conduit à la lenteur de la migration de la communauté :
Python 2 est un très bon langage. On ne répare pas ce qui marche.
Il y a beaucoup de code legacy en Python 2 et ça coûte cher de migrer.
La PSF a été trop gentille avec la communauté et l’a chouchoutée. En JS et Ruby ils ont dit “migrez ou allez vous faire foutre” et tout le monde a migré très vite.
Mais je pense que la raison principale c’est le manque de motivation pour le faire. Il n’y a pas un gros sticker jaune fluo d’un truc genre “gagnez 30% de perfs en plus” que les devs adorent même si ça n’influence pas tant leur vie que ça. Mais les codeurs ne sont pas rationnels contrairement à ce qu’ils disent. Ils aiment les one-liners, c’est pour dire.
Pourtant, il y a des tas de choses excellentes en Python 3. Simplement:
Elles ne sont pas sexy.
Elles ne se voient pas instantanément.
Personne n’en parle.
Après 2 ans de Python 3 quasi-fulltime, je peux vous le dire, je ne veux plus coder en Python 2. Et on va voir pourquoi.
Unicode, mon bel unicode
On a crié que c’était la raison principale. A mon avis c’est une erreur. Peu de gens peuvent vraiment voir ce que ça implique.
Mais en tant que formateur, voilà ce que je n’ai plus a expliquer:
Pourquoi un accent dans un commentaire fait planter le programme. Et # coding:
Pourquoi il faut faire from codecs import open et non pas juste open.
Pourquoirequest.get().read() + 'é' fait planter le programme. Et encode() et decode().
Pourquoi os.listdir()[0] + 'é' fait afficher des trucs chelous.
Pourquoi print(sql_row[0]) fait planter le programme ou affiche des trucs chelous. Ou les deux.
Et je peux supprimer de chacun de mes fichiers:
Tous les u ou les from __future__/codecs
Les en-tête d’encoding.
La moitié des encode/decode.
Et toutes les APIS ont un paramètre encoding, qui a pour valeur par défaut ‘UTF8′.
Debuggage for the win
Des centaines d’ajustements ont été faits pour faciliter la gestion des erreurs et le debuggage. Meilleures exceptions, plus de vérifications, meilleurs messages d’erreur, etc.
Quelques exemples…
En Python 2:
>>>[1,2,3]<"abc"True
En Python 3 TypeError: unorderable types: list() < str() of course.
En Python 2, IOError pour tout:
>>>open('/etc/postgresql/9.5/main/pg_hba.conf')
Traceback (most recent call last):
File "<stdin>", line 1,in<module>IOError: [Errno 13] Permission denied: '/etc/postgresql/9.5/main/pg_hba.conf'>>>open('/etc/postgresql/9.5/main/')
Traceback (most recent call last):
File "<stdin>", line 1,in<module>IOError: [Errno 21] Is a directory: '/etc/postgresql/9.5/main/'
En Python 3, c'est bien plus facile à gérer dans un try/except ou à debugger:
>>>open('/etc/postgresql/9.5/main/pg_hba.conf')
Traceback (most recent call last):
File "<stdin>", line 1,in<module>
PermissionError: [Errno 13] Permission denied: '/etc/postgresql/9.5/main/pg_hba.conf'>>>open('/etc/postgresql/9.5/main/')
Traceback (most recent call last):
File "<stdin>", line 1,in<module>
IsADirectoryError: [Errno 21] Is a directory: '/etc/postgresql/9.5/main/'
Les cascades d'exceptions en Python 3 sont très claires:
>>>try:
... open('/')# Erreur, c'est un dossier:
... exceptIOError:
... 1 / 0# oui c'est stupide, c'est pour l'exemple
...
Traceback(most recent call last):
File "<stdin>", line 2,in<module>
IsADirectoryError: [Errno 21] Is a directory: '/'
During handling of the above exception, another exception occurred:
File "<stdin>", line 4,in<module>ZeroDivisionError: division by zero
La même chose en Python 2:
Traceback (most recent call last):
File "<stdin>", line 4,in<module>ZeroDivisionError: integer division or modulo by zero
Bonne chance pour trouver l'erreur originale.
Plein de duplications ont été retirées.
En Python 2, un dev doit savoir la différence entre:
range() et xrange()
map/filter et itertools.imap/itertools.ifilter
dict.items/keys/values, dict.iteritems/keys/values et dict.viewitems/keys/values
open et codecs.open
Et md5 vs hashlib.md5
getopt, optparse, argparse
Cette manipulation de fichier se fait avec sys, os ou shutil ?
On hérite de UserDict ou dict ? UserList ou list ?
...
Sous peine de bugs ou de tuer ses perfs.
Certains bugs sont inévitables, et les modules csv et re sont par exemple pour toujours buggés en Python 2.
Good bye boiler plate
Faire un programme correct en Python 2 requière plus de code. Prenons l'exemple d'une suite de fichier qui contient des valeurs à récupérer sur chaque ligne, ou des commentaires (avec potentiellement des accents). Les fichiers font quelques centaines de méga, et je veux itérer sur leurs contenus.
Python 2:
# coding: utf8from__future__import unicode_literals, division
importosimportsysfrommathimport log
fromcodecsimportopenfromglobimportglobfromitertoolsimport imap # pour ne pas charger 300 Mo en mémoire d'un coup
FS_ENCODING =sys.getfilesystemencoding()
SIZE_SUFFIXES =['bytes','KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB']def file_size(size):
order =int(log(size,2) / 10)if size else0# potential bug with log2
size = size / (1<<(order * 10))return'{:.4g} {}'.format(size, suffixes[order])def get_data(dir, *patterns, **kwargs):
""" Charge les données des fichiers """# keyword only args
convert = kwargs.get('convert',int)
encoding = kwargs.get('encoding','utf8')for p in patterns:
for path inglob(os.path.join(dir, p)):
ifos.path.isfile(path):
upath = path.decode(FS_ENCODING, error="replace")print'Trouvé: ', upath, file_size(os.stat(path).st_size)withopen(path, encoding=encoding, error="ignore")as f:
# retirer les commentaires
lines =(l for l in f if"#"notin l)for value in imap(convert, f):
yield value
C'est déjà pas mal. On gère les caractères non ASCII dans les fichiers et le nom des fichiers, on affiche tout proprement sur le terminal, on itère en lazy pour ne pas saturer la RAM... Un code assez chouette, et pour obtenir ce résultat dans d'autres langages vous auriez plus dégueu (ou plus buggé).
Python 3:
# wow, so much space, much less importfrommathimport log2 # non buggy log2from pathlib import Path # plus de os.path bullshit
SIZE_SUFFIXES =['bytes','KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB']def file_size(size):
order =int(log2(size) / 10)if size else0
size = size / (1<<(order * 10))return f'{size:.4g} {suffixes[order]}'# fstringdef get_data(dir, *patterns, convert=int, encoding="utf8"): # keyword only""" Charge les données des fichiers """for p in patterns:
for path in Path(dir).glob(p):
if path.is_file():
print('Trouvé: ', path, file_size(path.stat().st_size))withopen(path, encoding=encoding, error="ignore")as f:
# retirer les commentaires
lines =(l for l in f if"#"notin l)yieldfrommap(convert, lines)# déroulage automatique
Oui, c'est tout.
Et ce n'est pas juste une question de taille du code. Notez tout ce que vous n'avez pas à savoir à l'avance pour que ça marche. Le code est plus lisible. La signature de la fonction mieux documentée. Et il marchera mieux sur une console Windows car le support a été amélioré en Python 3.
Et encore j'ai été sympa, je n'ai pas fait la gestion des erreurs de lecture des fichiers, sinon on en avait encore pour 3 ans.
Il y a des tas d'autres trucs.
L'unpacking généralisé:
>>> a, *rest, c =range(10)# récupérer la première et dernière valeur>>> foo(*bar1, *bar2)# tout passer en arg>>>{**dico1, **dico2}# fusionner deux dico :)
Ou la POO simplifiée:
class FooFooFoo(object):
...
class BarBarBar(FooFooFoo):
def wololo(self):
returnsuper(BarBarBar,self).wololo()
Devient:
class FooFooFoo:
...
class BarBarBar(FooFooFoo):
def wololo(self):
returnsuper().wololo()
Python 3 est tout simplement plus simple, et plus expressif.
Bonus
Evidement il y a plein de trucs qui n'intéresseront qu'une certaine catégorie de devs:
ipaddress.ip_address pour parser les adresses IP.
asyncio pour faire de l'IO non blocante sans threads.
enum des enum de toutes sortes.
functools.lru_cache pour cacher le résultat de ses fonctions.
type hints pour vérifier la validité de son code.
l'opérateur @ pour multiplier des matrices avec numpy.
concurrent.futures pour faire des pools non blocantes propres.
statistics stats performantes et correctes.
tracemalloc trouver les fuites de mémoire.
faulthandler gérer les crash du code C proprement.
Si vous n'êtes pas concernés, ça n'est pas motivant. Mais si ça vous touche personnellement, c'est super cool.
Au passage, depuis la 3.6, Python 3 est enfin plus rapide que Python 2 pour la plupart des opérations :)
Pour finir...
Toutes ces choses là s'accumulent. Un code plus court, plus lisible, plus facile à débugger, plus juste, plus performant. Par un tas de petits détails.
Alors oui, la migration ne va pas restaurer instantanément votre érection et vous faire perdre du poids. Mais sur le long terme, tout ça compte énormément.
Comme tous les 2 mois je parts en mission spéciale pour m’adonner aux pratiques les plus salaces afin d’assouvir mes pulsions DSKniennes.
Et comme 99.99% des mecs j’adore les éjacs faciales mais aussi sur les pieds (fétichisme oblige) ou les fesses ou encore une bonne paire de nichons bien juteux.
Il y a quelques années je m’étais renseigné sur des pilules pour augmenter le volume de sperme mais c’était du pipeau, j’ai donc regardé la composition du sperme et grosso modo la voici:
Le sperme contient de nombreux éléments nourriciers pour le spermatozoïde :
vitamines C et B12, sels minéraux comme le calcium, le magnésium, le phosphore, le potassium et le zinc, des sucres (fructose et sorbitol).
Je me suis penché sur les éléments les plus courants à trouver à savoir la vitamine C, B12, magnésium, zinc et fructose.
vitamine C = orange
B12 = banane
magnesium et zinc = cacao
fructose = les fruits
Je me suis donc mis en tête d’ingurgiter quotidiennement ces ingrédients afin de tester une éventuelle augmentation du volume spermique.
Mon test n’est basé sur absolument aucun étude scientifique, mais de toutes façons c’est que des produits naturels, et en plus ça file la pêche.
Bien évidement vous n’allez pas jouer au pompier du jour au lendemain mais j’ai pu noter une certaine augmentation du volume sans que ce soit non plus les chutes du niagara (le titre c’est juste pour être en premier dans les forums d’ados) mais je les trouves supérieures tout de même avec un certain avantage, une m’a dit que mon liquide séminal avait le goût de fruits, elle avait l’air d’en être plutôt contente.
Passons au cocktail !
2 fois par jour, en général 10h du math et 16/17h:
Dans un mixer versez:
1 banane
1 orange
une tête de menthe fraîche
un peu de lait d’amande
un mélange d’amandes pilées et de coco râpée (ça se trouve rayon patisserie)
du cacao en poudre
du miel
noix de cajou
cannelle
Mixez le tout pendant 5 minutes et servez frais !
Faut faire ce “régime” pendant au moins quelques semaines et vous devriez voir les premiers résultats, madame sera ravie.
Du 100% Bio ! Bon pour le teint.
chériiiiiiie, tes vitamines arrivent…
PS: j’ai fait plusieurs tests et c’est celui qui me donne le plus de satisfaction, si vous avez des variantes ou des suggestions allez-y. A côté de ça j’ai une alimentation qui exclue tout produit surgelé ou transformé par l’industrie agro alimentaire, paraît qu’on appelle ça le régime paléo, perso je fais pas de régime, je bouffe comme ma grand-mère et mes parents, pas de junk food et je me sens bien.
Je me gène pas me défoncer un plateau de fruits de mer à l’occas (au passage les huîtres sont blindées de zinc, B12, omega3 et ça a une texture de chatte alors pourquoi s’en priver ?!)
Pour les féministes défoulez-vous, mais n’oubliez pas vos cachets et le rendez-vous de demain chez le psy.
Il ne se passe pas un mois sans que je ne lise le commentaire d’un abruti qui annonce être capable de réécrire service X en quelques jours. Des projets et des projets de clones pour le prouver.
Twitter, Uber, Imgur, whatever.
Une variante est de juger la stack d’un service en prétendant qu’on pourrait faire beaucoup mieux avec moins.
Je ne sais pas si c’est de la bêtise, de l’ignorance ou de l’arrogance. Probablement des trois.
Prenons par exemple Twitter. Easy non ? Des messages de 140 caractères, quelques tags.
Mais, mais, mais les amis. Ca c’était Twitter le premier week-end de sa sortie aussi.
Maintenant Twitter c’est beaucoup plus que ça:
Suivi des messages en temps réel ou par historique complet.
Suivi par personne ou tag.
Possibilité de faire une recherche complexe sur des gens, tags ou du texte libre. Avec filtrage par settings comme la langue ou la date. Et obtenir les résultats en temps réel.
API complète pour accéder au service.
Analytics sur les tweets, personnes ou tags. Et l’auth, le bouton twitter, etc.
Auto-complétion partout.
UI responsive design, et compatible avec des dizaines de navigateurs.
Et une app mobile qui fonctionne sur 2 OS majeurs.
Upload de médias.
Minification d’URLs.
Instruments de modération et anti-spam.
Intégration de miniatures de contenus liés.
Intégrations avec services externes tels que Youtube, Slideshare, etc.
Comptes avec fonctionnalités professionnelles.
Bien entendu avec son succès Twitter doit maintenant scaler et assurer:
La connexion de centaines de millions d’utilisateurs chaque jour.
Qui postent et lisent des messages.
Qui peuvent être reçus par des millions d’utilisateurs. Et oui, c’est pas du chat. Un message ne va pas à une dizaine de destinataires.
Qui peuvent être reçus par des tags lus par des centaines de services en ligne. Et oui, en plus des abonnés, les messages sont aussi distribués par tag. Des millions de tags. Combinables arbitrairement.
L’historique de tous les tweets doit être accessible. Des milliards de tweet.
Gérer tout ce bordel alors que les messages sont un graphe. Et oui, entre les utilisateurs, les tags, les RT récursifs, les réponses et les likes, twitter n’est pas une timeline linéaire. C’est un graphe. Traité en quasi temps réel.
Se défendre contre les attaques, les abus du service, supporter le trending topics dûs à l’actualité.
Fournir le service dans des centaines de pays avec des langues, cultures, lois et situations politiques différentes.
Gérer la partie commerciale (faire de la thune) et marketing (préserver son image).
Gérer la boite : l’équipe, la thune, les locaux, les projets.
Gérer les incidents incongrus. Ben oui, si un truc à une chance de 0.0000000001% de se produire, sur un service qui a des centaines de millions de users qui s’activent comme les chimpanzés hystériques, ce truc arrive TOUS LES JOURS.
Aujourd’hui ce service, c’est des millions de lignes de code.
En prime, un service à succès n’est pas que du code, c’est aussi une énorme infra. Une logistique de dingue. Et un effort marketing colossal.
Personne ne peut reproduire ce que fait X actuellement en un an de travail. Je ne parle pas d’un week-end.
Les projets que l’on voit sont des “représentation du concept clé de X”.
Ca ne veut pas dire qu’il ne faut pas essayer de copier, concurrencer, proposer des alternatives, etc. Faut juste pas prendre les gens pour des cons.
Bref, à tous les prétendants au “c’est super simple” présents, passés et futurs :
Un bon article bien long. Je sens que ça vous avait manqué :) Musique ?
Un problème qui se retrouve souvent, c’est le besoin d’afficher un message qui contient des valeurs de variables. Or, si en Python on privilégie généralement “il y a une seule manière de faire quelque chose”, cela ne s’applique malheureusement pas au formatage de chaînes qui a accumulé bien des outils au fil des années.
TL;DR
Si c’est juste pour afficher 2, 3 bricoles dans le terminal, utilisez print() directement:
>>>print("J'ai",3,"ans")
J'ai 3 ans
>>> print(3, 2, 1, sep='-')
3-2-1
Si vous avez besoin d’un formatage plus complexe ou que le texte n’est pas que pour afficher dans le terminal…
Python 3.6+, utilisez les f-strings:
>>> produit ="nipple clamps">>> prix =13>>>print(f"Les {produit} coutent {prix:.2f} euros")
Les nipple clamps coutent 13.00 euros
Sinon utilisez format():
>>> produit ="nipple clamps">>> prix =13>>>print("Les {} coutent {:.2f} euros".format(produit, prix))
Les nipple clamps coutent 13.00 euros
Si vous êtes dans le shell, que vous voulez aller vite, ou que vous manipulez des bytes, vous pouvez utiliser “%”, mais si ça ne vous arrive jamais, personne ne vous en voudra:
>>> produit ="nipple clamps">>> prix =13>>>print("Les %s coutent %.2f euros" % (produit, prix))
Les nipple clamps coutent 13.00 euros
N’utilisez jamais string.Template.
Si vous avez un gros morceau de texte ou besoin de logique avancée, utilisez un moteur de template comme jinja2 ou mako. Pour l’i18n et la l10n, choisissez une lib comme babel.
Avec print()
Par exemple, si j’ai :
produit ="nipple clamps"
prix =13
Et je veux afficher :
"Les nipple clamps coutent 13 euros"
La manière la plus simple de faire cela est d’utiliser print():
>>>print("Les", produit,"coutent", prix,"euros")
Les nipple clamps coutent 13 euros
Mais déjà un problème se pose : cette fonction insère des espaces entre chaque argument qu’elle affiche. Cela est ennuyeux si par exemple je veux utiliser le signe € et le coller pour obtenir :
Les nipple clamps coutent 13€
print() possède un paramètre spécial pour cela : sep. Il contient le séparateur, c’est à dire le caractère qui va être utilisé pour séparé les différents arguments affichés. Par défaut, sep est égal à un espace.
Si je change ma phrase et que j’ai besoin d’espaces à certains endroits et pas d’autres il me faut définir un séparateur – ici une chaîne vide – et jouer un peu avec le texte :
Bon, mais admettons que je veuille sauvegarder ce texte dans une variable ? Par exemple pour le passer à une fonction qui vérifie l’orthographe ou met la phrase en jaune fluo…
Dans ce cas ça devient burlesque, il faut intercepter stdout et récupérer le résultat :
Vous l’avez compris, print() est fantastique pour les cas simples, mais devient rapidement peu pratique pour les cas complexes : son rôle est d’être bon à afficher, pas à formater.
Avec +
A ce stade, un débutant va généralement taper “concaténation string python” sur son moteur de recherche et tomber sur l’opérateur +. Il essaye alors ça :
>>>"Les " + produit + " coutent " + total + "€"
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)<ipython-input-19-126b156e23bd>in<module>()
---->1"Les " + produit + " coutent " + total + "€"TypeError: Can't convert 'float' object to str implicitly
Et il apprend par la même occasion que Python est fortement typé. On ne peut pas additionner des choux et des carottes disait ma prof de CE1, et donc on ne peut pas additionner "coutent" (type str) et total (type float).
C’est mieux que notre version avec print(), d’autant qu’on peut sauvegarde facilement tout ça dans une variable :
>>> total =round(prix + exo_taxe,2)>>> msg ="Les " + produit + " coutent " + str(total) + "€">>>print(msg)
Les nipple clamps coutent 13.01€
Mais ça reste chiant à taper, et encore plus à modifier. Si je veux insérer quelque chose là dedans, il me faut faire très attention en déplaçant mes + et mes " sans compter calculer ma gestion des espaces.
La raison est simple : il est difficile de voir la phrase que j’essaye d’afficher sans bien étudier mon expression.
Par ailleurs, je suis toujours obligé de faire mon arrondi.
Pour cette raison, je recommande de ne pas utiliser + pour formatter son texte, car il existe de bien meilleurs outils en Python.
Avec %
Là, on arrive à quelque chose de plus sympa !
L’opérateur % appliqué aux chaînes de caractères permet de définir un texte à trou, et ensuite de dire quoi mettre dans les trous. C’est une logique de template.
Elle est courte et pratique : c’est la méthode que j’utilise le plus actuellement dans un shell ou sur les chaînes courtes.
Par exemple, si je veux créer la chaîne:
Les nipple clamps coutent 13€
Alors mon texte à trou va ressembler à :
Les [insérer ici le nom du produit] coutent [insérer ici le prix du produit]€
Avec l’opérateur %, le texte à trou s’écrit :
Les %s coutent %s€
%s marque les trous.
Pour remplir, on met les variables à droite, dans l’ordre des trous à remplir :
En plus des marqueurs qui permettent de savoir où insérer la valeur et quel format lui donner, on peut aussi donner des précisions sur l’opération de formattage. On peut par ainsi décider de combien de chiffres après la virgule on souhaite, ou obliger la valeur à avoir une certaine taille :
>>>"%4d" % 28# au moins 4 caratères' 28'>>>"%04d" % 28# au moins 4 chiffres'0028'>>>"%.2f" % 28# 2 chiffres après la virgule'28.00'
On peut voir néanmoins que le pari n’est pas tout à fait gagné. Et on ne gagne pas tant que ça en lisibilité. Pour cette raison, les formatages complexes sont plus intéressants à faire avec format() que nous verrons plus loin.
Rappelez-vous néanmoins que depuis Python 3, format() ne fonctionne plus sur les bytes. % reste donc la seule option pour formater des paquets réseaux, des headers de jpeg et tout autre format binaire.
Formatter les dates
Même si il est toujours recommandé d’utiliser une bonne lib pour manipuler les dates, Python permet déjà de faire pas mal de choses avec la lib standard.
En effet, certaines notions, comme le temps, ont une forme très différente entre celle utilisée pour les manipuler, et celles utilisees pour les représenter.
Pour cette raison, l’objet date de Python propose deux méthodes, strptime et strftime, pour gérer le format des dates.
La procédure pour gérer les dates se fait donc toujours en 3 parties, un peu comme l’encoding d’un texte :
Créer une nouvelle date, soit à la main, soit à partir de données existantes.
Manipuler les dates pour obtenir ce qu’on souhaite (un autre date, un durée, un intervalle, etc.
Formater le résultat pour le présenter à l’utilisateur ou le sauvegarder à nouveau.
Pour récupérer une date existante, on va utiliser strptime (“str” pour string, “p” pour parse) :
>>fromdatetimeimportdatetime>>> date =datetime.strptime("1/4/2017","%d/%m/%Y")>>> date.year2017>>> date.day1
Le deuxième paramètre contient le motif à extraire de la chaine de gauche : c’est l’inverse d’un texte à trou ! On dit “dans la chaîne de gauche, j’ai le jour là, le mois là et l’année là, maintenant extrait les”.
Pour formater une date, c’est la même chose, mais dans l’autre sens, avec strftime (“f” pour format) :
>>> date =datetime.now()>>> date.strp>>> date.strftime('%m-%d-%y')'04-01-17'
% a ses limites. C’est un opérateur pratique pour les petites chaînes et les cas de tous les jours, mais si on a beaucoup de valeurs à formafter, cela peut devenir vite un problème. Il possède aussi quelques cas d’utilisation qui causes des erreurs inatendues puis qu’il n’accepte que les tuples.
format() a été créé pour remédier à cela. Dans sa forme la plus simple, il s’utilise presque comme %, mais les marqueurs sont des {} et non des %s :
Les f-strings sont une nouvelle fonctionnalité de Python 3.6, et elles sont merveilleuses, combinant les avantages de .format() et %, sans les inconvénients :
>>> produit ="nipple clamps">>> prix =13>>>print(f"Les {produit} coutent {prix:.2f} euros")
Les nipple clamps coutent 13.00 euros
En gros, c’est la syntaxe de format(), mais dans sa verbosité.
En prime, on peut utiliser des expressions arbitraires dedans:
Mais en bytecode. Pas d’injection de code Python possible, et en prime, les f-strings sont aujourd’hui la méthode formattage la plus performante.
En clair, si vous êtes en 3.6+, vous pouvez oublier toutes les autres.
Méthodes de l’objet str
Parfois, on ne veut pas remplir un texte à trou. Parfois on a déjà le texte et on veut le transformer. Pour cela, l’objet str possède de nombreuses méthodes qui permet de créer une nouvelle chaîne, qui possède des traits différents :
>>>" strip() retire les caractères en bouts de chaîne ".strip()# espace par défaut'strip() retire les caractères en bouts de chaîne'>>>"##strip() retire les caractères en bouts de chaîne"##".strip("#")'strip() retire les caractères en bouts de chaîne'>>>""##strip() retire les caractères en bouts de chaîne"##".lstrip("#")'strip() retire les caractères en bouts de chaîne"##'>>>""##strip() retire les caractères en bouts de chaîne"##".rstrip("#")'"##strip() retire les caractères en bouts de chaîne'>>>"wololo".replace('o','i')# remplacer des lettres'wilili'>>>"WOLOLO".lower()# changer la casse'wololo'>>>"wololo".upper()'WOLOLO'>>>"wololo".title()'Wololo'
Notez bien que ces méthodes créent de nouvelles chaînes. L’objet initial n’est pas modifié, puisque les strings sont immutables en Python.
Parmi les plus intéressantes, il y a split() et join(), qui ont une caractéristique particulière : elles ne transforment pas une chaîne en une autre.
split() prend une chaine, et retourne… une liste !
>>>"split() découpe une chaîne en petits bouts".split()# défaut sur espaces['split()','découpe','une','chaîne','en','petits','bouts']>>>"split() découpe une chaîne en petits bouts".split("e")['split() découp',' un',' chaîn',' ','n p','tits bouts']
join() fait l’inverse, et prend un itérable (comme une liste), pour retourner… une chaîne :)
Quand on écrit "" en Python on ne crée pas une chaîne. En fait, on écrit une instruction qui dit à Python comment créer une chaîne.
La différence est subtile, mais importante. "" n’est PAS la chaîne, "" est une instruction, une indication pour Python de comment il doit procéder pour créer une chaîne.
Et on peut donner des instructions plus précises à Python. Par exemple on peut dire, “insère moi ici un saut de ligne”. Cela se fait avec le marqueur "\n".
>>>print('un saut\n de ligne')
un saut
de ligne
\n n’est PAS un saut de ligne. C’est juste une indication donnée à Python pour lui dire qu’ici, il doit insérer un saut de ligne quand il créera la chaîne en mémoire.
Il existe plusieurs marqueurs de ce genre, les plus importants étant \n (saut de ligne) et \t (tabulation).
Pour rentrer les caratères \t\n, il faut donc dire à Python explicitement qu’on ne veut pas qu’il insère un saut de ligne ou une tabulation, mais plutôt ces caractères.
Cela peut se faire, soit avec le caractères d’échappement \ :
>>>print('pas un saut\\n de ligne')
pas un saut\n de ligne
Soit en désactivant cette fonctionalité avec le préfixe r, pour raw string:
>>>print(r'pas un saut\n de ligne')
pas un saut\n de ligne
Cette fonctionalité est très utilisée pour les noms de fichiers Windows et les expressions rationnelles car ils contiennent souvent \t\n.
Bytes, strings et encoding
Python fait une distinction très forte entre les octets (type bytes) et le texte (type str). La raison est qu’il n’existe pas de texte brut dans la vraie vie, et que tout ce que vous lisez : fichiers, base de données, socket réseau, et même votre code source (!) est un flux d’octets encodés dans un certain ordre pour représenter du texte.
En Python, on a donc le type str pour représenter du texte, une forme d’abtraction de toute forme d’encodage qui permet de manipuler ses données textuelles sans se soucier de comment il est représenté en mémoire.
En revanche, quand on importe du texte (lire, télécharge, parser, etc) ou qu’on exporte du texte (écrire, afficher, uploader, etc), il faut explicitement convertir son texte vers le type bytes, qui lui a un encoding en particulier.
Ce principe mérite un article à lui tout seul, et je vous renvoie donc à la page dédiée du blog.
Templating
Parfois on a beaucoup de texte à gérer. Par exemple, si vous faites un site Web, vous aurez beaucoup de HTML. Dans ce cas, faire tout le formatage dans son fichier Python n’est pas du tout pragmatique.
Pour cet usage particulier, on utilise ce qu’on appelle un moteur de template, c’est à dire une bibliothèque qui va vous permettre de mettre votre texte à trou dans un fichier à part. Les moteurs de templates sophistiqués vous permettent de faire quelques opérations logiques comme des boucles ou des conditions dans votre texte.
La première chose à savoir, c’est de ne PAS utiliser string.Template. Cette classe ne permet d’utiliser aucune logique, n’est n’a aucun avantage par rapport à .format().
Pour le templating, il vaut mieux se pencher vers une lib tièrce partie. Les deux principaux concurrents sont Jinja2, le moteur de templating le plus populaire en Python, créé par l’auteur de Flask. Et le moteur de Django, fourni par défaut par le framework.
Depuis Django 1.10, le framework supporte aussi jinja2, donc je vais vous donner un exemple avec ce dernier. Sachez qu’il existe bien d’autres moteurs (mako, cheetah, templite, TAL…) mais jinja2 a plus ou moins gagné la guerre.
On fait son template dans un fichier à part, par exemple wololo.txt:
Regardez je sais compter :
{% for number in numbers %}
- {{number}}
{% endfor %}
Puis en Python:
import jinja2
# On Définit où trouver les fichiers de template. Ex, le dossier courant:
jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader('.'))# On dit à jinja de charger le template à partir de son chemin relatif
template = jinja_env.get_template('wololo.txt')# On crée un contexte, c'est à dire une collection d'objects qu'on veut rendre# accessibles dans le template. Généralement, c'est un dictionnaire dont les# clés sont les noms des variables telles qu'elle apparaitrons dans le template# et les valeurs ce que contiendrons ces variables.
ctx ={"numbers": [1,2,3]}# On demande le "rendu" du template, c'est à dire le mélange du template# et du contexte.
resultat = template.render(ctx)print(resultat)# Ce qui donne :# - 1# - 2# - 3
i18n et l10n
L’i18n, pour ‘internationalisation’ (soit 18 lettres entre le i et le n) est le fait d’organiser votre code de telle sorte que son interface puisse s’adapter à plusieurs cultures. La l10n, pour ‘localisation’ (soit 10 lettres en le l et le n), est le fait de fournir avec son code les données nécessaires pour une culture en particulier.
Par exemple, marquer toutes vos chaines de caractères comme étant traductibles et fournir un mécanisme de substitution de la chaine est de l’i18n. Fournir un fichier de traduction pour l’espagnol pour ces chaînes est de la l10n.
La combinaison des deux est parfois nommée g11n pour “globalization”.
La g11n peut inclure:
La gestion de l’UI (traduction, sens de la lecture, formatage des nombres, devise…)
La gestion des dates (formatage, différences de type de calendriers, événements locaux, ordre des jours…).
La gestion du temps (zones horaires, heure d’été…)
La gestion de la géolocation (fournir des informations autour de soi, filtrer par la distance…).
La gestion polique et culturelle (symbolisme des couleurs, adaptation du contenu aux moeurs…).
La gestion légale (services et contenus selon la loi en vigueur, warnings obligatoires…)
Plus qu’un article, c’est un dossier qu’il faudrait faire sur ces sujets car c’est très, très vaste.
La traduction de texte peut être faite directement avec le module gettext fournit en Python. Certains formatages de nombres et de dates sont aussi faisables avec la stdlib grace au module locale.
Néanmoins dès que vous voulez faire quelque chose de plus gros avec la g11n, je vous invite à vous tourner vers des libs externes.
Babel est la référence en Python pour le formatage du texte et des nombres, et il existe des extensions pour les moteurs de template les plus populaires. La lib inclut une base de données aussi à jour que possible sur les devises, les noms de pays, les langues…
pendulum est idéal pour la manipulation des dates en général, et des fuseaux horaires en particulier, y compris pour le formatage. Et ça évite de manipuler pytz à la main.
__repr__ est utilisé quand on appelle repr() sur un objet. Typiquement, c’est ce qui s’affiche dans le shell si on utilise pas print(). C’est aussi ce qui détermine la représentation d’un objet quand on affiche une collections qui le contient.
__str__ est utilisée quand on appelle str() sur un objet. Quand on fait print() dessus par exemple. Si __str__ n’existe pas, __repr__ est appelé.
__format__ est utilisé quand on passe cette objet à format(), ou que cet objet est utilisé dans une f-string.
Ex :
class Foo:
def__repr__(self):
return"<Everybody's kung foo fighting>"def__str__(self):
return"C'est l'histoire d'un foo qui rentre dans un bar"def __format__(self, age):
ifint(age or0)>18:
return"On s'en bat les couilles avec une tarte tatin. Tiède."return"On s'en foo"
Ce qui donne :
>>> Foo()<Everybody's kung foo fighting>
>>> print(Foo())
C'est l'histoire d'un foo qui rentre dans un bar
>>>print([Foo(), Foo()])[<Everybody's kung foo fighting>, <Everybody's kung foo fighting>]>>>"J'ai envie de dire: {}".format(Foo())"J'ai envie de dire: On s'en foo">>> f"J'ai envie de dire: {Foo():19}""J'ai envie de dire: On s'en bat les couilles avec une tarte tatin. Tiède."
Astuce de dernière minute
Enfin pour conclure cet article dont la longueur n’a d’égale que celle de la période entre deux publications sur le blog, une petite remarque.
S’il est certes courant de formatter une string, il est aussi possible de déformer un string. Ce sont des pièces plus résistantes qu’il n’y parait, et en cas d’empressement, le retrait total n’est pas nécessaire :
Ne pas porter de strings du tout évite aussi tout une classe de bugs
Assurez-vous juste que la partie ficelle soit suffisament éloignée pour éviter les frictions fort désagréables quand on entame un algo avec une grosse boucle.
Sinon, moins intéressant, mais toujours utile, les strings en Python peut êtres écrites sur plusieurs lignes de plusieurs manières:
>>> s =("Ceci est une chaine qui n'a pas "
... "de saut de ligne mais qui est "
... "écrite sur plusieurs lignes")>>>print(s)
Ceci est une chaine qui n'a pas de saut de ligne mais qui est écrite sur plusieurs lignes
Cela fonctionne car deux chaines litérales cote à cote en Python sont automatiquement concaténées au démarage du programme. Cela évite les + \ à chaque fin de ligne, pourvu qu’on ait des parenthèses de chaque côté de la chaîne.
L’alternative des triples quotes est assez connue:
>>> s ="""
... Ceci est une chaine avec des sauts de lignes
... écrite sur plusiers lignes.
... """>>>print(s)
Ceci est une chaine avec des sauts de lignes
écrite sur plusiers lignes.
Pour éviter l’indentation et les espaces inutiles:
>>>fromtextwrapimport dedent
>>>print(dedent(s).strip())
Ceci est une chaine avec des sauts de lignes
écrite sur plusiers lignes.