string – Sam & Max http://sametmax.com Du code, du cul Wed, 23 Dec 2020 13:35:02 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Le type bytes n’est pas du texte http://sametmax.com/le-type-bytes-nest-pas-du-texte/ Fri, 11 Jan 2019 11:30:10 +0000 http://sametmax.com/?p=25125 je craque. Mais je me soigne, globalement j'ai récupéré plein de temps, et ça se voit sur mon quotidien. Et ce craquage, et bien il est cette fois dû à une totale mécompréhension des types de texte en Python 3.]]> J’ai beau essayer très fort de ne pas répondre en ligne, des fois je craque. Mais je me soigne, globalement j’ai récupéré plein de temps, et ça se voit sur mon quotidien.

Et ce craquage, et bien il est cette fois dû à une totale mécompréhension des types de texte en Python 3.

Mais c’est bien normal: Python 3 ne gère pas le texte de la même manière que la grande majorité des langages de programmation, justement à cause de la débâcle qu’on a eue en Python 2. Du coup, de nombreux programmeurs arrivent avec leur expérience d’ailleurs, et tentent de l’appliquer tel un utilisateur de SVN migrant sur git. En surface ça semble coller, malheuseuement à l’usage, ça fait faire des erreurs.

Donc un peu d’explications.

En informatique, tout est une histoire de convention. On dit que tel mot clé a tel effet. Que tel nom suppose telle chose. Que tel code de retour implique telle erreur. Que tel schéma XML représente tel type de document.

Essentiellement, tout cela est arbitraire: des gens ont décidé qu’il en serait ainsi. Impossible de deviner que ce que fait yield ou with si vous n’avez pas d’expérience similaire avant. Impossible de savoir que le code 0 en bash ou 200 en HTTP signifie tout va bien sans qu’on vous transmette l’information, ou faire de nombreux tests.

Quand je dis arbitrairement, évidemment je ne veux pas dire complètement sans raison. Il y a des raisons techniques, politiques, économiques, et parfois esthétiques à ces conventions. Cela n’en retire en rien l’aspect parfaitement artificiel de ces choix.

La convention la plus omniprésente, et pourtant aujourd’hui la plus masquée dans un monde où on utilise massivement des langages de haut niveau comme Javascript, Ruby, PHP et Python, est celle de l’organisation des octets.

Musique !

…je vois même plus le code : tout ce que je vois, c’est des blondes, des brunes, des rousses.

Tout ce qui passe par nos ordinateurs n’est qu’une suite de zéros et de uns, que nous avons groupés par paquets de 8:

Seulement la grande révélation, le “aaaaaaahhhhh okayyyyyyy” qui arrive un jour dans toute vie de dev, c’est que ces paquets de 8 ne veulent rien dire. Rien. C’est nous qui avons décidé, arbitrairement encore une fois, de leur signification.

Vous voyez ce moment dans les films et séries où un personnage arrive à “lire du binaire” ?

Evidement, "c'est une representation binaire ASCII de coordonnées WGS 84 Web Mercator" est plus dur à caser dans un dialogue

Evidement, “c’est une representation binaire ASCII de coordonnées WGS 84 Web Mercator” est plus dur à caser dans un dialogue

C’est de l’enculage de dauphin.

Le binaire n’est pas un langage, pas plus que les lettres “abcdefghijklmnopqrstuvwxyz”. Vous pouvez utiliser ces lettres pour représenter certains mots italiens, français, anglais, un nom propre (sans langue), le label d’un immeuble (sans langue encore) ou un chiffre latin.

Que veut dire “les gosses” ? Pour la même combinaisons de lettres, cela signifie “les enfants” avec la convention française européenne, et “les couilles” avec la convention québéquoise.

Pour le binaire c’est pareil, ce que veut dire un octet dépend de la convention que vous avez choisie.

Par exemple, que signifie cette suite d’octets ?

1100001 1100010 1100011 1100100

Bah rien. Mais on peut lui donner un sens en lui appliquant une convention.

Je peux lui appliquer la convention ASCII, et donc supposer que c’est un texte dans un certain format. Voici ce que ça donne en Python:

     
>>> data = bytearray([0b1100001, 0b1100010, 0b1100011, 0b1100100])     
>>> print(data.decode('ascii'))     
abcd 
Les processeurs modernes ne comprenent pas nativement l'american apparel

Les processeurs modernes ne comprenent pas nativement l’american apparel

Ou je peux lui appliquer une autre convention, et decider de lire ces octets comme si ils étaient le dump d’une structure C. Interprettons en Python ces octets comme un entier non signé en big-endian:

     
>>> data = bytearray([0b1100001, 0b1100010, 0b1100011, 0b1100100])     
>>> import struct     
>>> struct.unpack('>I', data)     
(1633837924,)

Même suite de bits, mais selon la convention choisie, elle veut dire les lettres “abcd” ou le nombre “1633837924”. Et oui, comme il n’y a pas une infinité de combinaisons de 0 et de 1 qui tiennent dans un espace mémoire limité, différentes conventions vont utiliser les mêmes octets mais décider que ça veut dire quelque chose de différent.

En fait, même des conventions pour le même type usage ne veulent pas forcément dire la même chose. Par exemple, prenez l’octet:

11101001

Un octet somme toute sympathique, de bonne famille. Il ne paie pas de mine, mais c’est un membre utile de la société.

Et maintenant, quelqu’un vous donne un indice, il vous dit que cet octet représente… du texte.

Super !

Oui, mais du texte avec quelle convention ? Car les pays du monde entier ont créé leur propre convention pour représenter du texte.

Avec la convention “latin-1”, utilisé par 0.7% de tous les sites Web du monde ?

 
>>> bytearray([0b11101001]).decode('latin-1') 
'é' 

Avec la convention “cp850”, utilisé par la console DOS ?

 
>>> bytearray([0b11101001]).decode('cp850')
'Ú'

Vous voulez rire ? Le premier à remplacé presque partout le second parce qu’ils contiennent les mêmes lettres. Elles ne sont juste pas représentées par la même combinaison d’octets.

Et cet octet, que veut-il dire avec la convention “utf8”, qui est aujourd’hui le standard international recommandé pour représenter du texte ?

 
>>> bytearray([0b11101001]).decode('utf8')
Traceback (most recent call last):
File "", line 1, in 
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 0: unexpected end of data 

Il n’a pas de correspondance. Cet octet n’est pas de l’utf8 valide.

Si vous voulez représenter ces lettres en utf8, il faut utiliser une convention différente, en utilisant non pas un seul octet, mais une séquence d’octets:

 
>>> list(map(bin, 'é'.encode('utf8')))
['0b11000011', '0b10101001']
>>> list(map(bin, 'Ú'.encode('utf8')))
['0b11000011', '0b10011010']

Vous pourriez croire que puisque le texte est particulièrement compliqué, c’est normal d’avoir des conventions qui divergent. Mais non, c’est juste la nature des conventions. Puisqu’elles sont arbitraires, l’une n’est pas plus “la vérité” qu’une autre. On retrouve la même chose avec les nombres:

>>> struct.unpack("h", bytearray([0b11101001, 0b11101001]))
(-5655,)
>>> struct.unpack("H", bytearray([0b11101001, 0b11101001])) 
(59881,)

La même suite d’octets peut représenter deux nombres totalement différents, selon que je décide de les lire comme des “short”, ou des “unsigned short”.

Et l’inverse est aussi vrai.

Ben oui, si quelque chose peut être interprété de plusieurs façons, on a aussi le fait que deux représentations différentes peuvent être interprétées … pour aboutir au même résultat.

Par exemple, le nombre des doigts de ma main peut être représenté de plein de façons différentes:

  • décimal: 5
  • français écrit: cinq
  • chiffre latin: V
  • anglais écrit: five
  • espagnol écrit: cinco
  • base deux: 101
  • structure C d’un signed short en little-endian avec Python: bytearray([0b101, 0b0])

Que de manières différentes, pour le même concept ! En plus, il y a confusion possible: V est une lettre également. cinq, five et cinco utilisent le même alphabet, mais pas les mêmes symboles spécifiques, pour représenter la même chose. Et le plus confusionant, 101 est une représentation binaire, mais bytearray([0b101, 0b0]) aussi.

Bref, voilà toute la complexité de la différence entre la donnée, un concept abstrait qui n’existe pas, et sa représentation, une convention humaine concrète qui nous permet de communiquer entre nous.

Donc, pour lire “du binaire”, ou faire n’importe quoi en informatique, il faut connaitre la convention utilisée. Mais pas juste en informatique: pour lire le journal, il faut connaitre la convention des symboles imprimés sur les pages, pour conduire sans se faire tuer, il faut connaitre la convention des panneaux, et pour parler, il faut connaitre la convention de la compression des molécules d’air émise par l’appareil buccal et respiratoire d’un individu qui vient rencontrer votre système auditif.

Vous êtes un être très conventionnel au fond.

Évidemment on trouve la même chose en Python. Par exemple vous pouvez utiliser plusieurs conventions pour demander à Python de créer le même nombre en mémoire:

>>> 245 # base 10
245
>>> 0xF5 # hexadecimal
245
>>> 0b11110101 # binaire
245
>>> 245 == 0xF5 == 0b11110101
True     
>>> type(245)     
     
>>> type(0xF5)     
     
>>> type(0b11110101)     
 

Inversement, "1" et 1 paraissent similaire, mais ils ont différents buts. Le premier est un outil destiné à l’affichage, qui matérialise le caractère représentant le chiffre arabe après le zéro. Il est stocké en interne avec une séquence d’octets similaire à:

>>> bin(ord("1"))
'0b110001'

Tandis que que le second est un outil fait pour faire des calculs avec la plus petite valeur positive entière non nulle. Il est stocké en interne avec une séquence d’octets similaire à:

>>> list(map(bin, struct.pack('l', 1)))
['0b1', '0b0', '0b0', '0b0', '0b0', '0b0', '0b0', '0b0']

Je simplifie bien entendu, en vérité la representation interne des nombres et du texte en Python est plus complexe que cela, et dépend de l’implémentation choisie, du type de processeur, de la taille de la donnée et de votre configuration.

Retour sur le type bytes

J’ai soigneusement évité d’utiliser le type bytes durant cette démonstration, le remplaçant techniquement inutilement (mais pédagogiquement brillamment, car je suis génial) par bytearray.

En effet, toute cette leçon est là pour arriver à la conclusion que bytes ne représente pas du texte, mais si je vous avais montré tout ça avec lui, voilà qui vous aurait interloqué:

     
>>> bytes([0b1100001, 0b1100010, 0b1100011, 0b1100100])     
b'abcd' 

“Heu, mais c’est du texte !” me dirait alors un lecteur ayant diagonalisé l’article.

Mais bien entendu que non.

bytes ne présente pas du texte, c’est une structure de données dont le but est de permettre de manipuler une séquence d’octets ordonnée, et ce manuellement. N’importe laquelle.

Or, il se trouve que beaucoup de langages de programmation représentent le texte comme un array d’octets, et y attachent quelques opérations de manipulation. C’est le cas du C, ou de Python 2 par exemple. Les gens ayant eu cette expérience pensent donc que b'abcd' représente du texte, allant parfois jusqu’à aller lui donner l’appellation de “byte string”.

Il n’existe rien de tel en Python 3.

En Python 3, vous avez deux types pour manipuler des séquences d’octets: bytes et bytearray. Ils sont équivalents, à ceci près que bytes est non mutable (non modifiable) alors que bytearray est mutable (modifiable).

Ces types peuvent contenir n’importe quels octets, et nous avons vu ensemble qu’une même séquence d’octets pouvait être interprétée différemment selon la convention choisie pour la lire. Évidemment il est préférable de la lire avec la même convention qui a été utilisée pour la produire, sans quoi on ne comprendra pas ce que le producteur de la donnée à voulu dire.

Sauf que…

Beaucoup d’outils en informatique utilisent les conventions ASCII et hexadécimale pour symboliser les valeurs des octets. Si vous lancez Wireshark pour regarder les paquets d’un protocole réseau ou si vous ouvrez un PNG avec xxd, on va vous représenter le contenu avec un mélange de ces conventions.

Pour des raisons pratiques, Python fait donc la même chose, et permet ainsi de visualiser (ou produire) le type bytes à l’aide d’une notation ASCII:

    
>>> print(b'abcd'.decode('ascii'))     
abcd     
>>> struct.unpack('>I', b'abcd')     
(1633837924,)

Ou d’une notation héxa (ironiquement, l’héxa est representé par une combinaison de caractères ASCII \o/) si les valeurs ne tiennent pas dans la table ASCII:

     
>>> "é".encode('utf8')  # hexa C3 A9   
b'\xc3\xa9'     
>>> struct.unpack('h', b'\xc3\xa9')    
(-22077,)

Donc bytes, bien qu’il puisse contenir des octets interprétables comme du texte, n’est pas particulièrement fait pour manipuler du texte. Il peut contenir n’importe quoi. Mais pour des raisons pratiques, sa représentation dans le terminal est faite avec une convention familière. Après tout, il faut bien l’écrire en quelque chose pour l’affiquer à l’écran.

Si on veut manipuler du texte en Python 3, il faut utiliser le type str, qui est l’outil spécialisé dans la representation et la manipulation textuelle. Si vous savez qu’un type bytes contient des octets qui representent du texte, alors utilisez la méthode décode() avec la bonne convention (appelée “charset”), pour récupérer un str:

     
>>> print(b'P\xc3\xa8re No\xc3\xabl'.decode('utf8'))
Père Noël 

On a un très bon article sur l’encoding en Python sur le blog, d’ailleurs.

Toute cela n’était bien entendu pas vrai en Python 2. En Python 2, le type str était un array d’octets, rendant tout cela bien confus, et amenant à plein d’erreurs. L’introduction lors de la version 2.0 de l’objet unicode pour pallier le problème, bien que très utile, n’a fait que rajouter à l’incomprehension des nouveaux venus.

Or le monde extérieur, lui, n’a pas d’abstraction pour le texte. Faire des abstractions, c’est le rôle du langage de programmation. Si vous écrivez dans un terminal, ou lisez depuis un terminal, un nom de fichier, le contenu d’une base de données, une requête AJAX, etc., ce sont évidemment des octets qui sont échangés, et il vous faut la bonne convention pour faire partie de la discussion.

Le type bas niveau bytes est un outil qui sert donc à communiquer avec le monde extérieur, tandis que les types haut niveau (str, int, list, etc.) sont des outils qui font l’abstraction de ces conventions, pour vous permettre de manipuler confortablement un concept général (du texte, un nombre, une collection ordonnée) à l’interieur des murs de votre programme.

]]>
25125
Le formatage des strings en long et en large http://sametmax.com/le-formatage-des-strings-en-long-et-en-large/ http://sametmax.com/le-formatage-des-strings-en-long-et-en-large/#comments Mon, 16 Jan 2017 14:53:04 +0000 http://sametmax.com/?p=21963 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} coûtent {prix:.2f} euros")
Les nipple clamps coûtent 13.00 euros

Sinon utilisez format():

>>> produit = "nipple clamps"
>>> prix = 13
>>> print("Les {} coûtent {:.2f} euros".format(produit, prix))
Les nipple clamps coûtent 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 coûtent %.2f euros" % (produit, prix))
Les nipple clamps coûtent 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 coûtent 13 euros"

La manière la plus simple de faire cela est d’utiliser print():

>>> print("Les", produit, "coûtent", prix, "euros")
Les nipple clamps coûtent 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 coûtent 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éparer 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 :

>>> print("Les ", produit, " coûtent ", prix, "€", sep="")
Les nipple clamps coûtent 13€

C’est mieux. Mais, ça commence à devenir moins lisible.

Maintenant que se passe-t-il si je veux utiliser une valeur numérique mais que j’ai besoin de la formater ?

Par exemple :

produit = "nipple clamps"
prix = 13
exo_taxe = 0.011

Et je veux tronquer le prix au centime de telle sorte que j’obtienne :

Les nipple clamps coûtent 13.01€

Arf, ça va demander un peu plus de travail.

>>> total = round(prix + exo_taxe, 2)
>>> print("Les ", produit, " coûtent ", total, "€", sep="")

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 :

>>> faux_terminal = io.StringIO()
>>> print("Les ", produit, " coûtent ", total, "€", sep="", file=faux_terminal)
>>> faux_terminal.seek(0)
>>> msg = faux_terminal.read()
>>> print(msg)
Les nipple clamps coûtent 13.01€

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 + " coûtent " + total + "€"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in ()
----> 1 "Les " + produit + " coûtent " + 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 "coûtent" (type str) et total (type float).

Il faut donc convertir total :

>>> "Les " + produit + " coûtent " + str(total) + "€"
'Les nipple clamps coûtent 13.01€'

C’est mieux que notre version avec print(), d’autant qu’on peut sauvegarder facilement tout ça dans une variable :

>>> total = round(prix + exo_taxe, 2)
>>> msg = "Les " + produit + " coûtent " + str(total) + "€"
>>> print(msg)
Les nipple clamps coûtent 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 formater 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 à trous, 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 coûtent 13€

Alors mon texte à trou va ressembler à :

Les [insérer ici le nom du produit] coûtent [insérer ici le prix du produit]€

Avec l’opérateur %, le texte à trous s’écrit :

Les %s coûtent %s€

%s marque les trous.

Pour remplir, on met les variables à droite, dans l’ordre des trous à remplir :

>>> total = round(prix + exo_taxe, 2)
>>> "Les %s coûtent %s€" % (produit, total)
'Les nipple clamps coûtent 13.01€'

Pas besoin de convertir total en str, et la phrase qu’on souhaite obtenir est facile à deviner en lisant l’expression.

%s veut dire “met moi ici la conversion en str de cet objet”. C’est comme si on appelait str(total).

Il existe d’autres marqueurs :

  • %d est comme si faisait int() sur la valeur.
  • %f est comme si faisait float() sur la valeur.
  • %x est comme si on faisait hex()[2:]
  • etc

Ex:

>>> "%d" % 28.01
    '28'
>>> "%f" % 28
    '28.000000'
>>> "%x" % 28
    '1c'

La liste des marqueurs est disponible sur cette page de la doc.

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 formatage. On peut ainsi décider combien de chiffres après la virgule on souhaite, ou obliger la valeur à avoir une certaine taille :

>>> "%4d" % 28 # au moins 4 caractères
    '  28'
>>> "%04d" % 28 # au moins 4 chiffres
    '0028'
>>> "%.2f" % 28 # 2 chiffres après la virgule
    '28.00'

Ainsi notre exemple:

>>> total = round(prix + exo_taxe, 2)
>>> "Les %s coûtent %s€" % (produit, total)
    'Les nipple clamps coûtent 13.01€'

peut maintenant être réduit à :

>>> total = prix + exo_taxe
>>> "Les %s coûtent %.2f€" % (produit, total)
    'Les nipple clamps coûtent 13.01€'

Néanmoins un des défauts de % est qu’il n’accepte qu’un tuple, ou une valeur seule. Impossible de passer un itérable arbitraire :

>>> "Les %s coûtent %.2f€" % [produit, total]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in ()
----> 1 "Les %s coûtent %.2f€" % [produit, total]

TypeError: not enough arguments for format string

Et si vous voulez formater un tuple, il faut le mettre dans un tuple d’un seul élément, source de plantage :

>>> "Les données sont %s" % data
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in ()
----> 1 "Les données sont %s" % data

TypeError: not all arguments converted during string formatting

>>> "Les données sont %s" % (data, )
    "Les données sont ('nipple clamps', 13.01)"

Tout codeur Python s’est retrouvé un jour devant ce cas et s’est gratté la tête.

Par ailleurs, dès qu’il y a beaucoup de trous à combler dans le texte, ça devient vite difficile de savoir ce qui va où :

 "[%s] %s%s %s(%s) - %s%s%s"  % (
        datetime.datetime.now(),
        res,
        unit,
        type,
        variant,
        testers[0],
        testers[1],
        testers[2]
  )

Pour pallier ce problème, % peut accepter aussi un dictionnaire et avoir des trous nommés :

 "[%(date)s] %(value)s%(unit)s %(type)s(%(variant)s) - %(tester1)s%(tester2)s%(tester3)s"  % {
        "date": datetime.datetime.now(),
        "value": res,
        "unit": "m",
        "type": "3",
        "variant": "beta",
        "tester1": testers[0],
        "tester2": testers[1],
        "tester3": testers[2]
  }

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.

Formater 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 utilisées 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 :

  1. Créer une nouvelle date, soit à la main, soit à partir de données existantes.
  2. Manipuler les dates pour obtenir ce qu’on souhaite (un autre date, un durée, un intervalle, etc.
  3. 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) :

>>> from datetime import datetime
>>> date = datetime.strptime("1/4/2017", "%d/%m/%Y")
>>> date.year
    2017
>>> date.day
    1

Le deuxième paramètre contient le motif à extraire de la chaîne de gauche : c’est l’inverse d’un texte à trous ! On dit “dans la chaîne de gauche, j’ai le jour là, le mois là et l’année là, maintenant extrais 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.strftime('%m-%d-%y')
    '04-01-17'

Le mini-langage pour formater les dates est documenté ici, et vous pouvez en apprendre plus sur les dates sur une petite intro dédiée.

Avec format()

% 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 à formater, cela peut devenir vite un problème. Il possède aussi quelques cas d’utilisation qui causent des erreurs inattendues puisqu’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 {} coûtent {}€".format(produit, prix)
    'Les nipple clamps coûtent 13€'

Mais déjà, format() se distingue du lot car il permet de choisir l’ordre d’insertion :

>>> "Les {1} coûtent {0}€".format(prix, produit)
    'Les nipple clamps coûtent 13€'

La méthode accepte également n’importe quel itérable grâce à l’unpacking :

>>> "Les {} coûtent {}€".format(*[produit, prix])
    'Les nipple clamps coûtent 13€'

Formater un tuple seul est aussi très simple :

>>> "Les données sont {}".format(data)
    "Les données sont ('nipple clamps', 13.01)"

Mais là où format() est bien plus pratique, c’est quand on a beaucoup de données et qu’on veut nommer ses trous :

 "[{date}] {value}{unit} {type}({variant}) - {testers[0]}{testers[1]}{testers[2]}".format(
        date=datetime.datetime.now(),
        value=res,
        unit="m",
        type="3",
        variant="beta",
        testers=testers
  )

Le texte à trous est plus clair, et on peut utiliser l’index d’une liste directement dedans.

Un autre avantage non négligeable, est que format() n’utilise pas de nombreux marqueurs différents comme %f, %d, %s

A la place, il n’y a que {}, et format() appelle en fait pour chaque valeur sa méthode … __format__, ou en l’absence de celle-ci appelle str().

>>> a = 42
>>> a.__format__("")
    '42'

Chaque objet peut définir __format__, et accepter ses propres options:

>>> a.__format__(".2f")
    '42.00'
>>> from datetime import datetime
>>> datetime.now().__format__('%d %h')  # pas besoin de strftime !
    '20 Sep'

Et format() utilise tout ce qui est après : dans un trou pour le passer à __format__:

>>> "{foo:.2f} {bar:%d %h}".format(foo=42, bar=datetime.now())

Cela permet des formatages très poussés.

Les f-strings

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} coûtent {prix:.2f} euros")
Les nipple clamps coûtent 13.00 euros

En gros, c’est la syntaxe de format(), mais sans sa verbosité.

En prime, on peut utiliser des expressions arbitraires dedans:

>>> print(f"Les {produit.upper()} coûtent {prix:.2f} euros")
Les NIPPLE CLAMPS coûtent 13.00 euros

A première vue, ça ressemble à du exec, et donc à un parsing lent, doublé d’une grosse faille de sécurité.

Et bien non !

C’est en fait du sucre syntaxique, et au parsing du code, Python va transformer l’expression en un truc du genre:

"Les " + "{}".format(produit.upper()) + " coûtent " + "{:.2f}".format(prix) + " euros"

Mais en bytecode. Pas d’injection de code Python possible, et en prime, les f-strings sont aujourd’hui la méthode de formatage 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 à trous. Parfois on a déjà le texte et on veut le transformer. Pour cela, l’objet str possède de nombreuses méthodes qui permettent 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 chaîne, 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 :)

>>> "-".join(['join()', 'recolle', 'une', 'chaîne', 'DEPUIS', 'des', 'petits', 'bouts'])
    'join()-recolle-une-chaîne-DEPUIS-des-petits-bouts'

Avec juste ces méthodes, on peut s’autoriser pas mal de fantaisies avec le texte, et le nombre de méthodes disponibles est assez large :

>>> dir(str)
    [...
 'capitalize',
 'casefold',
 'center',
 'count',
 'encode',
 'endswith',
 'expandtabs',
 'find',
 'format',
 'format_map',
 'index',
 'isalnum',
 'isalpha',
 'isdecimal',
 'isdigit',
 'isidentifier',
 'islower',
 'isnumeric',
 'isprintable',
 'isspace',
 'istitle',
 'isupper',
 'join',
 'ljust',
 'lower',
 'lstrip',
 'maketrans',
 'partition',
 'replace',
 'rfind',
 'rindex',
 'rjust',
 'rpartition',
 'rsplit',
 'rstrip',
 'split',
 'splitlines',
 'startswith',
 'strip',
 'swapcase',
 'title',
 'translate',
 'upper',
 'zfill']

Donc référez-vous à la doc.

Caractères spéciaux

On vous a menti !

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ère 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’abstraction 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écharger, 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 à trous 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, et 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.

Un coup de pip :

pip install jinja2

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 apparaîtront dans le template
# et les valeurs ce que contiendront 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 entre 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 chaînes de caractères comme étant traductibles et fournir un mécanisme de substitution de la chaîne 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 types de calendriers, événements locaux, ordres 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 politique 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 fourni en Python. Certains formatages de nombres et de dates sont aussi faisables avec la stdlib grâce 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.

Et pour le reste… bonne chance !

Programmation orientée objet

Souvenez-vous, Python a des méthodes magiques. 3 sont dédiées au formatage.

__repr__ est utilisée 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 collection 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ée.

__format__ est utilisée quand on passe cet objet à format(), ou que cet objet est utilisé dans une f-string.

Ex :

class Foo:
    def __repr__(self):
        return ""
    def __str__(self):
        return "C'est l'histoire d'un foo qui rentre dans un bar"
    def __format__(self, age):
        if int(age or 0) > 18:
            return "On s'en bat les couilles avec une tarte tatin. Tiède."
        return "On s'en foo"

Ce qui donne :

>>> Foo()

>>> print(Foo())
C'est l'histoire d'un foo qui rentre dans un bar
>>> print([Foo(), Foo()])
[, ]
>>> "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 formater 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 :

String en levrette

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 peuvent ê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 chaînes littérales côte à côte en Python sont automatiquement concaténées au démarrage 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 plusieurs lignes.   
... """
>>> print(s)

    Ceci est une chaine avec des sauts de lignes
    écrite sur plusieurs lignes.

Pour éviter l’indentation et les espaces inutiles:

>>> from textwrap import dedent
>>> print(dedent(s).strip())
Ceci est une chaine avec des sauts de lignes
écrite sur plusieurs lignes.

Perso j’ai un wrapper pour ça.

]]>
http://sametmax.com/le-formatage-des-strings-en-long-et-en-large/feed/ 19 21963
Article retiré pour cause de grosse merde http://sametmax.com/nested-format-expansion-thats-a-swag-title-cracker/ http://sametmax.com/nested-format-expansion-thats-a-swag-title-cracker/#comments Thu, 19 Dec 2013 08:10:32 +0000 http://sametmax.com/?p=8158 Désolé pour ceux qui ont reçu l’article via RSS ou email. Je le retire. C’était de la merde.

]]>
http://sametmax.com/nested-format-expansion-thats-a-swag-title-cracker/feed/ 4 8158
Les expressions rationnelles en Python, parfois overkill http://sametmax.com/les-expressions-rationnelles-en-python-parfois-overkill/ http://sametmax.com/les-expressions-rationnelles-en-python-parfois-overkill/#comments Wed, 07 Aug 2013 11:40:11 +0000 http://sametmax.com/?p=7042 J’adore les regex, et d’ailleurs il faudra que je fasse une série d’articles sur le sujet, un peu comme le guide de la POO.

Mais dans un langage comme Python, il y a de nombreuses solutions à mettre en oeuvre avant d’utiliser les regex.

Pour vérifier si une chaîne est dans une autre, utilisez in :

>>> 'a' in 'chat'
True
>>> 'a' in 'chien'
False
>>> 'a' in 'CHAT'.lower() # ignorer la casse
True

Pour savoir si un chaîne est au début ou à la fin, utilisez startswith() et endswith() :

>>> 'achat'.startswith('a')
True
>>> 'chat'.startswith('a')
False
>>> 'acheta'.endswith('a')
True

Pour savoir si la chaîne est d’un type particulier, utiliser les méthodes is* :

>>> '555'.isdigit()
True
>>> ''.isdigit()
False
>>> '⑦'.isdigit()
True
>>> '444'.isdecimal()
True
>>> '444.55'.isdecimal()
False
>> '⑦'.isdecimal()
False
>>> '879fds'.isalpha()
False
>>> 'fsdqfsqd'.isalpha()
True
>>> 'fsdqfsqd'.islower()
True
>>> 'fsdqFFsqd'.islower()
False
>>> '879fds'.isalnum()
True
>>> '879fds-'.isalnum()
False
>>> ' \t\n'.isspace()
True
>>> ' \t\n fdsfd'.isspace()
False

Si vous voulez manipuler la chaîne pour en extraire une partie, utilisez split() (ou rsplit(), lsplit() pour travailler sur la droite ou la gauche de la chaîne) :

>>> s = """Mais, vous savez, moi je ne crois pas qu'il y ait de bonne ou de mauvaise situation. Moi, si je devais résumer ma vie aujourd'hui avec vous, je dirais que c'est d'abord des rencontres, des gens qui m'ont tendu la main, peut-être à un moment où je ne pouvais pas, où j'étais seul chez moi. Et c'est assez curieux de se dire que les hasards, les rencontres forgent une destinée... Parce que quand on a le goût de la chose, quand on a le goût de la chose bien faite, le beau geste, parfois on ne trouve pas l'interlocuteur en face, je dirais, le miroir qui vous aide à avancer. Alors ce n'est pas mon cas, comme je le disais là, puisque moi au contraire, j'ai pu ; et je dis merci à la vie, je lui dis merci et je chante la vie, je danse la vie... Je ne suis qu'amour ! Et finalement, quand beaucoup de gens aujourd'hui me disent "Mais comment fais-tu pour avoir cette humanité ?", et bien je leur réponds très simplement, je leur dis que c'est ce goût de l'amour, ce goût donc qui m'a poussé aujourd'hui à entreprendre une construction mécanique, mais demain, qui sait, peut-être simplement à me mettre au service de la communauté, à faire le don... le don de soi."""
>>> s.split()
[u'Mais,', u'vous', u'savez,', u'moi', u'je', u'ne', u'crois', u'pas', u"qu'il", u'y', u'ait', u'de', u'bonne', u'ou', u'de', u'mauvaise', u'situation.', u'Moi,', u'si', u'je', u'devais', u'r\xe9sumer', u'ma', u'vie', u"aujourd'hui", u'avec', u'vous,', u'je', u'dirais', u'que', u"c'est", u"d'abord", u'des', u'rencontres,', u'des', u'gens', u'qui', u"m'ont", u'tendu', u'la', u'main,', u'peut-\xeatre', u'\xe0', u'un', u'moment', u'o\xf9', u'je', u'ne', u'pouvais', u'pas,', u'o\xf9', u"j'\xe9tais", u'seul', u'chez', u'moi.', u'Et', u"c'est", u'assez', u'curieux', u'de', u'se', u'dire', u'que', u'les', u'hasards,', u'les', u'rencontres', u'forgent', u'une', u'destin\xe9e...', u'Parce', u'que', u'quand', u'on', u'a', u'le', u'go\xfbt', u'de', u'la', u'chose,', u'quand', u'on', u'a', u'le', u'go\xfbt', u'de', u'la', u'chose', u'bien', u'faite,', u'le', u'beau', u'geste,', u'parfois', u'on', u'ne', u'trouve', u'pas', u"l'interlocuteur", u'en', u'face,', u'je', u'dirais,', u'le', u'miroir', u'qui', u'vous', u'aide', u'\xe0', u'avancer.', u'Alors', u'ce', u"n'est", u'pas', u'mon', u'cas,', u'comme', u'je', u'le', u'disais', u'l\xe0,', u'puisque', u'moi', u'au', u'contraire,', u"j'ai", u'pu', u';', u'et', u'je', u'dis', u'merci', u'\xe0', u'la', u'vie,', u'je', u'lui', u'dis', u'merci', u'et', u'je', u'chante', u'la', u'vie,', u'je', u'danse', u'la', u'vie...', u'Je', u'ne', u'suis', u"qu'amour", u'!', u'Et', u'finalement,', u'quand', u'beaucoup', u'de', u'gens', u"aujourd'hui", u'me', u'disent', u'"Mais', u'comment', u'fais-tu', u'pour', u'avoir', u'cette', u'humanit\xe9', u'?",', u'et', u'bien', u'je', u'leur', u'r\xe9ponds', u'tr\xe8s', u'simplement,', u'je', u'leur', u'dis', u'que', u"c'est", u'ce', u'go\xfbt', u'de', u"l'amour,", u'ce', u'go\xfbt', u'donc', u'qui', u"m'a", u'pouss\xe9', u"aujourd'hui", u'\xe0', u'entreprendre', u'une', u'construction', u'm\xe9canique,', u'mais', u'demain,', u'qui', u'sait,', u'peut-\xeatre', u'simplement', u'\xe0', u'me', u'mettre', u'au', u'service', u'de', u'la', u'communaut\xe9,', u'\xe0', u'faire', u'le', u'don...', u'le', u'don', u'de', u'soi.']
>>> s.split()[0]
u'Mais,'
>>> s.split()[5:7]
[u'ne', u'crois']
>>> s.split(',')
[u'Mais', u' vous savez', u" moi je ne crois pas qu'il y ait de bonne ou de mauvaise situation. Moi", u" si je devais r\xe9sumer ma vie aujourd'hui avec vous", u" je dirais que c'est d'abord des rencontres", u" des gens qui m'ont tendu la main", u' peut-\xeatre \xe0 un moment o\xf9 je ne pouvais pas', u" o\xf9 j'\xe9tais seul chez moi. Et c'est assez curieux de se dire que les hasards", u' les rencontres forgent une destin\xe9e... Parce que quand on a le go\xfbt de la chose', u' quand on a le go\xfbt de la chose bien faite', u' le beau geste', u" parfois on ne trouve pas l'interlocuteur en face", u' je dirais', u" le miroir qui vous aide \xe0 avancer. Alors ce n'est pas mon cas", u' comme je le disais l\xe0', u' puisque moi au contraire', u" j'ai pu ; et je dis merci \xe0 la vie", u' je lui dis merci et je chante la vie', u" je danse la vie... Je ne suis qu'amour ! Et finalement", u' quand beaucoup de gens aujourd\'hui me disent "Mais comment fais-tu pour avoir cette humanit\xe9 ?"', u' et bien je leur r\xe9ponds tr\xe8s simplement', u" je leur dis que c'est ce go\xfbt de l'amour", u" ce go\xfbt donc qui m'a pouss\xe9 aujourd'hui \xe0 entreprendre une construction m\xe9canique", u' mais demain', u' qui sait', u' peut-\xeatre simplement \xe0 me mettre au service de la communaut\xe9', u' \xe0 faire le don... le don de soi.']
>>> s.split('.')
[u"Mais, vous savez, moi je ne crois pas qu'il y ait de bonne ou de mauvaise situation", u" Moi, si je devais r\xe9sumer ma vie aujourd'hui avec vous, je dirais que c'est d'abord des rencontres, des gens qui m'ont tendu la main, peut-\xeatre \xe0 un moment o\xf9 je ne pouvais pas, o\xf9 j'\xe9tais seul chez moi", u" Et c'est assez curieux de se dire que les hasards, les rencontres forgent une destin\xe9e", u'', u'', u" Parce que quand on a le go\xfbt de la chose, quand on a le go\xfbt de la chose bien faite, le beau geste, parfois on ne trouve pas l'interlocuteur en face, je dirais, le miroir qui vous aide \xe0 avancer", u" Alors ce n'est pas mon cas, comme je le disais l\xe0, puisque moi au contraire, j'ai pu ; et je dis merci \xe0 la vie, je lui dis merci et je chante la vie, je danse la vie", u'', u'', u' Je ne suis qu\'amour ! Et finalement, quand beaucoup de gens aujourd\'hui me disent "Mais comment fais-tu pour avoir cette humanit\xe9 ?", et bien je leur r\xe9ponds tr\xe8s simplement, je leur dis que c\'est ce go\xfbt de l\'amour, ce go\xfbt donc qui m\'a pouss\xe9 aujourd\'hui \xe0 entreprendre une construction m\xe9canique, mais demain, qui sait, peut-\xeatre simplement \xe0 me mettre au service de la communaut\xe9, \xe0 faire le don', u'', u'', u' le don de soi', u'']

Et n’oubliez pas que vous pouvez appeler join() derrière.

Si vous devez altérer la chaîne, utilisez strip() (et rstrip(), lstrip()) ou replace() :

>>> "Les nouilles cuisent au jus de canne".replace('noui', 'coui').replace('cui', 'nui').replace('jus', 'cul').replace('canne', 'jeanne')
u'Les couilles nuisent au cul de jeanne'
>>> "                               .                       ".strip()
u'.'
>>> "===                               .                       ======".strip("= ")
u'.'

En plus, les chaînes sont itérables, indexables et sliceables, donc :

>>> s = """And I will strike down upon thee with great vengeance and furious anger those who attempt to poison and destroy my brothers. And you will know my name is the Lord when I lay my vengeance upon you!"""
>>> s[3:30:3]
u' wltkdnp '
>>> s.split()
[u'And', u'I', u'will', u'strike', u'down', u'upon', u'thee', u'with', u'great', u'vengeance', u'and', u'furious', u'anger', u'those', u'who', u'attempt', u'to', u'poison', u'and', u'destroy', u'my', u'brothers.', u'And', u'you', u'will', u'know', u'my', u'name', u'is', u'the', u'Lord', u'when', u'I', u'lay', u'my', u'vengeance', u'upon', u'you!']
>>> s[0]
u'A'
>>> ''.join([(l.upper() if i % 2 else l) for i, l in enumerate(s)])
u'ANd I wIlL StRiKe dOwN UpOn tHeE WiTh gReAt vEnGeAnCe aNd fUrIoUs aNgEr tHoSe wHo aTtEmPt tO PoIsOn aNd dEsTrOy mY BrOtHeRs. AnD YoU WiLl kNoW My nAmE Is tHe LOrD WhEn I lAy mY VeNgEaNcE UpOn yOu!'

Bref, avant de sortir le bazooka, souvenez-vous que vous avez un arsenal déjà très approprié pour traiter les strings, dont les perfs seront en plus probablement meilleures.

]]>
http://sametmax.com/les-expressions-rationnelles-en-python-parfois-overkill/feed/ 15 7042
% ou format() en Python ? http://sametmax.com/ou-format-en-python/ http://sametmax.com/ou-format-en-python/#comments Fri, 21 Jun 2013 10:47:41 +0000 http://sametmax.com/?p=6416 Cet article a été mis à jour et contient maintenant du code Python en version 3

On a reçu un mail du genre :

Salut les mecs!

Je me demandais si il valait mieux utiliser format() ou % quand on veut insérer une variable dans une chaîne?
Je comprend pas vraiment quelle est la différence entre les deux…l’un est-il plus rapide? Plus fiable?

Merci les mecs!

Je suppose que d’autres personnes se posent la même question, du coup je poste ça là une bonne fois pour toutes.

En résumé : avec le recul, il n’y en a pas de meilleur. C’est une question de facilité d’usage et de lisibilité.

En effet, % était parti pour être déprécié en Python 3, mais ce n’est jamais arrivé, pour plusieurs raisons :

  • % est souvent plus court à taper.
  • le module logging utilise toujours ce format.
  • avec Python 3.5, le type bytes va de nouveau utiliser cet opérateur pour le formatage.

Si votre formatage est simple ou si vous utilisez des bytes ou le module logging, utilisez %:

>>> "je suis hyper %s. Brouuuuahhh" % "content"
    'je suis hyper content. Brouuuuahhh'

L’équivalent avec format() serait :

>>>  "je suis hyper {}. Brouuuuahhh".format("content")
    'je suis hyper content. Brouuuuahhh'

On y gagne pas, c’est plus long à taper car il y a plus de lettres mais aussi parce que qu’il y a beaucoup plus de caractères spéciaux à atteindre sur son clavier : {}.() contre %.

On table ici sur la facilité et la rapidité d’usage.

Si votre formatage a beaucoup de variables, que vos variables sont déjà dans un dictionnaire, que votre texte est long ou que vous avez besoin de formats avancés, utilisez format():

"{value}{unit} ({time:%H:%M:%S})".format(value=3, unit="ppm", time=datetime.now())
'3ppm (10:53:04)'

L’équivalent avec % serait:

date = datetime.now().strftime("%H:%M:%S")
"%(value)s%(unit)s (%(time)s)" % {"value": 3, "unit": "ppm", "time": date}

Moins lisible, plus verbeux. format() gagne toujours dès qu’on a un message complexe. Mais ça n’arrive pas aussi souvent qu’on ne le pense, du coup % a encore de beaux jours devant lui.

Avec Python 3.6 arrivera une nouvelle manière de faire, les f-strings. Si vous avez la chance d’utiliser la 3.6, les f-strings peuvent remplacer avantageusement la plupart des formes ci-dessus :

>>> value = 3
>>> unit = "ppm"
>>> f'{value}{unit} ({datetime.now():%H:%M:%S})'
'3ppm (10:53:54)'

Mais il faudra attendre 2016…

]]>
http://sametmax.com/ou-format-en-python/feed/ 26 6416
Le piège de la méthode strip() des chaînes en Python http://sametmax.com/le-piege-de-la-methode-strip-des-chaines-en-python/ http://sametmax.com/le-piege-de-la-methode-strip-des-chaines-en-python/#comments Mon, 01 Oct 2012 13:27:45 +0000 http://sametmax.com/?p=2309 strip(), et ses acolytes lstrip() et rstrip(), ne retirent pas la chaîne de caractères aux extrémités d'une autre chaîne de caractères.]]> strip(), et ses acolytes lstrip() et rstrip(), ne retirent pas la chaîne de caractères aux extrémités d’une autre chaîne de caractères.

Elles retirent des extrémités toutes lettres qui sont dans la chaînes passée en paramètre. La nuance est subtile, mais c’est la différence entre ça:

>>> "# ##  # test ".strip('# ') # comportement attendu mais faux
'##  # test '

Et ça:

>>> "# ##  # test ".strip('# ') # comportement réel
'test'
]]>
http://sametmax.com/le-piege-de-la-methode-strip-des-chaines-en-python/feed/ 7 2309
Comment marchent les “raw strings” en Python ? http://sametmax.com/comment-marchent-les-raw-strings-en-python/ http://sametmax.com/comment-marchent-les-raw-strings-en-python/#comments Tue, 06 Mar 2012 16:13:04 +0000 http://sametmax.com/?p=241 Cet article a été mis à jour et contient maintenant du code Python en version 3

Dans certains tutos, notamment ceux sur les expressions rationnelles, on recommande d’utiliser les “raw strings”, en mettant un “r” devant la déclaration de la chaîne de caractères.

Par exemple :

'1?\d\d?'

Devient :

r'1?\d\d?'

À quoi cela sert-il ?

Voyons d’abord à quoi cela ne sert pas

  • Créer un type de string particulier. Il n’y a rien de tel qu’un type “raw string” en Python. La chaîne résultante est une chaîne ordinaire
  • Créer une chaîne destinée aux regexes. On utilise particulièrement la notation ‘r’ avec les regexs, mais ce n’est pas un type dédié

‘r’ est juste un modificateur, une sorte de paramètre. En effet, quand vous écrivez 'Salut !\n', vous n’écrivez PAS la chaîne “Salut ![LB]”. Vous dites à Python de créer un objet chaîne, de la même manière que vous lui demanderiez une instanciation en faisant MaClasse().

'Salut !\n' est juste une notation demandant de créer une chaîne, pas la chaîne elle-même. Cette notation dit à Python : À partir de cet instant dans le programme, tu vas créer un objet chaîne en mémoire, et voici les paramètres que je te donne pour le créer. La notation étant différente, on a l’illusion d’écrire la chaîne soi-même, mais en fait ce n’est pas différent d’un appel de fonction.

Quand vous faites 'Salut !\n', vous dites plus précisément à Python : Instancie un objet de type string, met les caractères S, a, l, u, t, espace et point d’exclamation dedans, suivi d’un saut de ligne. Cette notation, pour se faciliter la vie, permet de décrire “saut de ligne” en écrivant '\n'. Python analyse donc votre chaîne, cherche toutes les combinaisons de caractères spéciaux comme '\n', '\t', etc, et quand il crée l’objet en mémoire, il ajoute un saut de ligne ou une tabulation, et pas les caractères ‘\’ puis ‘n’ ou ‘t’.

Que se passe-t-il si vous voulez réellement ajouter ‘\’ et ‘n’ ?

Il faut utiliser une autre notation, l’échappement. On utilise un ‘\’ pour dire à Python: créer cet objet en mémoire, mais à cet endroit, ne tient pas compte de la combinaison de caractères spéciaux.

Ceci affiche un saut de ligne : print('\n est le caractère de saut de ligne')

Ceci affiche ‘\’, ‘n’ puis la phrase : print('\\n est le caractère de saut de ligne')

Mais il existe des cas où c’est très fastidieux et illisible. Notamment les expressions rationnelles, où les ‘\’ font partie intégrante du système.

Un exemple simple, vous voulez “C:\Program Files” dans une phrase :

Votre regex devra contenir ‘\P’. Sauf qu’en regex, ‘\P’ est un symbole spécial, donc il faut l’échapper, vous aurez donc ‘\\P’. Sauf qu’en Python, il faut échapper les ‘\’ pour qu’ils ne soient pas considérés comme caractères spéciaux, vous aurez donc ‘\\\\P’. Sur une regex complexe, ça devient vite très moche, et très dur à déboguer.

C’est là qu’intervient le modificateur ‘r’, qui précise à Python : quand tu vas créer cette chaîne en mémoire, met ces caractères dedans littéralement et considère qu’il n’y a aucune combinaison de caractères spéciaux.

En clair r'\n' sera ‘\’ puis ‘n’. Python ne va tout simplement par parser la chaîne, il va l’utiliser littéralement, d’où le “raw” string.

Il n’y a donc rien de spécial dans une chaîne créée avec le modificateur ‘r’, c’est une chaîne normale, qui est instanciée sans réfléchir par Python, sans chercher à être malin et comprendre des notations spéciales.

Ça ne veut pas dire qu’on ne peut pas avoir des sauts de ligne dans une chaîne créée avec ‘r’ :

>>> print(r"""1 + 1 = 
... 42""")
1 + 1
42

Ça veut juste dire que les notations spéciales ne seront pas analysées :

>>> print('1 + 1 =\n 42')
1 + 1 =
42
>>> print(r'1 + 1 =\n 42')
1 + 1 =\n 42

Il est facile de s’embrouiller les pinceaux à cause du shell Python :

>>> 'test\n'
'test\n'
>>> r'test\n'
'test\\n'
>>> print('test\n')
test

>>> print(r'test\n')
test\n

Il faut savoir que quand on affiche un objet sans utiliser print() dans le shell, ce dernier essaye de vous afficher une représentation de l’objet de telle sorte qu’il puisse être copié et collé par un dev, et recréer le même objet. En revanche, print() va afficher du texte formaté pour être lisible par un utilisateur final.

Enfin, les combinaisons de préfixes peuvent ou non être compatibles:

>>> ur'test\n' 
  File "", line 1
    ur'test\n'
           ^
SyntaxError: invalid syntax
>>> rb'test\n'
    b'test\\n'

Donc si vous faites un code compatible Python 2 et 3 en même temps, faites gaffe.

Si les fstrings sont acceptées, ‘f’ sera compatible avec ‘r’.

]]>
http://sametmax.com/comment-marchent-les-raw-strings-en-python/feed/ 7 241