En Python 3, le type bytes est un array d’entiers


Le plus gros changement quand on passe de Python 2 à Python 3, c’est la gestion des chaînes de caractères.

Pour rappel :

  • En 2.7, les chaînes sont par défaut des arrays d’octets, et il faut les décoder pour obtenir de l’unicode.
  • En 3, les chaînes sont par défaut de type ‘unicode’, et il faut les encoder pour obtenir de un array d’octets.

Si vous avez besoin d’une mise à jour sur l’encoding en Python, on a un article pour ça.

Comme toute entrée ou sortie est forcément un flux d’octets, mais pas forcément dans le même encodage, Python 2.7 pouvait poser problème pour le débutant qui essayait de comprendre pourquoi son programme plantait, bordel de merde.

La version 3 prend plusieurs mesures pour éviter les bugs vicieux liés à l’encodage de caractères:

  • L’encodage par défaut du code est UTF8.
  • L’encodage par défaut de lecture et d’écriture est UTF8.
  • On ne peut plus mélanger ‘bytes’ et ‘unicode’.
  • Les messages d’erreur expliquent clairement et tôt tout problème.

La plupart du temps, quand on va manipuler du texte, on va donc toujours manipuler de l’unicode, en Python 3. Ce dernier va nous forcer à faire le décodage / encodage au bon moment.

Mais il restera quelques fois le besoin de manipuler du bytes, et ce type a subi un lifting…

La base

Créer un array d’octets (le type bytes‘, en Python 3) demande de préfixer une chaîne avec ‘b’ :

>>> s = b'I am evil, stop laughing!'
>>> type(s)
<class 'bytes'>
>>> print(s)
b'I am evil, stop laughing!'

Première remarque, on ne peut plus utiliser ce type pour afficher quoi que ce soit, puisque l’affichage est une représentation du type (appel à __repr__), et pas du texte mis en forme. Déjà Python vous indique la couleur : si vous voulez manipulez du texte, n’utilisez pas ce type.

Comparez avec le type unicode :

>>> u = s.decode('utf8')
>>> type(u)
<class 'str'>
>>> print(u)
I am evil, stop laughing!

L’affichage marche comme on s’y attend. Bref, vous êtes forcé de toujours rester sur de l’unicode (le type str en Python 3, ce qui porte à confusion) si vous manipulez du texte. Heureusement, c’est quasiment toujours ce que vous aurez.

Par exemple, si vous ouvrez un fichier en Python 3 :

>>> content = open('/etc/fstab').read()
>>> type(content)
<class 'str'>

C’est du texte. A moins de demander qu’il soit ouvert en mode binaire :

>>>> content = open('/etc/fstab', 'rb').read()
>>> type(content)
<class 'bytes'>

Une autre différence MAJEURE, c’est que, si dans Python 2.7, les arrays d’octets pouvaient être manipulés comme un array de lettres :

>>> s = 'I put the goal in golem...' 
>>> s[0] # en Python 2.7
>>> 'I'

En Python 3, les array d’octets sont au mieux manipulables comme un array d’entiers :

>>> s = b'I put the goal in golem...'
>>> s[0] # en Python 3
73

La représentation sous forme de lettre est gardée pour l’initialisation pour des raisons pratiques, mais sous le capot, il se passe ça:

>>> bytes([73, 32, 112, 117, 116, 32, 116, 104, 101, 32, 103, 111, 97, 108, 32, 105, 110, 32, 103, 111, 108, 101, 109, 46, 46, 46])
b'I put the goal in golem...'

D’ailleurs, on ne peut même plus faire d’opérations de formatage avec des octets comme en Python 2.7 :

>>> b"Welcome to the league of %s" % input('')
Draven
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for %: 'bytes' and 'str'

format() ne marche pas non plus. On est assez proche du tableau d’octets en C, sauf qu’en plus, on ne peut pas le modifier :

>>> s = b"My right arm is a lot stronger than my left arm."
>>> s[0] = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment

Les arrays d’octets sont donc maintenant essentiellement des outils de communication avec le monde extérieur.

Bytearray

Il existe encore des raisons de manipuler des arrays d’octets : les applications scientifiques. Typiquement, les algos de crypto opérent sur des arrays d’octets.

Pour cette raison, Python 3 vient également avec un nouveau type de base : bytearray, un array d’octets modifiable.

>>> s = bytearray(b"this tasted purple !")
>>> s[2:4] = b'at'
>>> print(s)
bytearray(b'that tasted purple !')

Et on a toutes les opérations de liste dessus, comme append, pop(), etc :

>>> for x in b' ,puY':
...     s.insert(0, x)
... 
>>> print(s)
bytearray(b'Yup, that tasted purple !')

Attention par contre, ces opérations attendent un entier en paramètres et NON un array d’octets.

Et un dernier détail :

>>> isinstance(bytes, bytearray)
False
>>> isinstance(bytearray, bytes)
False

Différence entre string et array d’octets

Il est facile de confondre tout ce merdier.

En Python 2.7, le type str était un array d’octets, et on le manipulait comme une chaîne, d’où la difficulté de transition.

En Python 3, bien qu’on puisse créer un array d’octets avec une syntaxe utilisant des lettres, ils ne sont plus du tout utilisés pour la manipulation de texte. Si vous voulez manipuler du texte qui vient de l’extérieur de votre programme, il faudra toujours le décoder pour obtenir un type str (qui est l’ancien type unicode de Python 2.7).

Le décodage sera fait automatiquement dans la plupart des cas, et plantera si on tombe sur un cas où vous devez le faire à la main et que vous ne le faites pas. Du coup, plus de difficulté à trouver d’où vient ce bug d’encoding, car on a toujours l’erreur à la source.

En ce sens, Python 3 est beaucoup plus clair : les octets d’un côté, le texte de l’autre. Bon, tout ça c’est de la surcouche, au final, tout est octet. Mais on a rarement envie de manipuler un octet directement, sinon on coderait encore en assembleur.

Avec ce système, Python 3 est le langage le plus sain que j’ai pu rencontrer dans sa gestion de l’encodage : il ne cache rien, oblige l’utilisateur à coder avec de bonnes habitudes, facilite le débugage et met sur le devant de la scène la problématique de l’encoding, qui est le plus souvent cachée vite fait sous le tapis.

L’alternative intelligente la plus proche étant celle de node.js, qui interdit tout simplement la plupart des encodings dans son API.

La bonne nouvelle ? 99% du temps, vous n’aurez même pas à vous en soucier, car ASCII est inclus dans UTF8, et ce sont les encodings les plus utilisés. Avec Python 3 forçant UTF8 par défaut partout et des chaînes en unicode dès le départ, il n’y a presque rien à faire. Je doute que la plupart des gens aient même à manipuler le type bytes.

19 thoughts on “En Python 3, le type bytes est un array d’entiers

  • ZED

    C’est pas plutôt “Array d’octet” ? Bytes = octets non ? 1 byte = 8 bits ?

    Après c’est pas la taille du byte qui compte….

    :D

  • k3c

    Merci pour cet article, de la part d’un retardataire encore en Python 2.
    Le sodomiseur de coléoptères a noté
    s/et ce type a subit un lifting/et ce type a subi un lifting
    s/que j’ai pu rencontré/que j’ai pu rencontrer
    s/comme une chaînes,/comme une chaîne,

  • Recher

    »» isinstance(bytes, bytearray)
    False
    »» isinstance(bytes, bytearray)
    False

    C’est pas pour dire, mais ce bout de code là, “ça fait deux fois ‘chacals’ “.

    C’est pas plutôt isinstance(bytearray, bytes) que tu voulais tester la deuxième fois ?
    D’ailleurs ça donnerait quoi ce code ? J’ai pas de python 3 sous la main.

    Je crois que je vais finir par essayer de m’y mettre, à ce fameux python 3.

  • kontre

    Il ne manquerait pas une inversion, là :

    >>> isinstance(bytes, bytearray)
    False
    >>> isinstance(bytes, bytearray)
    False

  • Greizgh

    Merci de mettre en lumière les types bytes.
    Et sinon: s/pour obtenir de un array de octets./pour obtenir un array d’octets.

  • Krypted

    D’ailleurs je viens de tomber sur cet article :

    J’avoue que je n’y ai pas compris grand chose. Quelle est la différence entre une liste et un array?

    Je crois qu’en Java une liste est un type particulier d’array, mais j’ai dû mal à saisir ce que les listes ont en plus.

  • Sam Post author

    Une liste est juste un type d’array

    Un array est une structure de données de bas niveau. La distance entre deux adresses de deux éléments voisins est constante dans tout l’array (généralement tous les éléments on le même type pour qu’ils prennent la même place).

    Une liste est une séquence d’élément finie.

    On voit bien que l’array est une question d’implémentation, la liste est une question de sémantique.

    Maintenant, le point de l’auteur de l’article cité est biaisé, car il ne dit pas si il définit la liste du point de vue de l’implémentation (ni de laquelle), de l’API ou du concept sémantique.

    A la fin il conclut de manière prétentieuse :

    At this point, the student attains enlightment and starts applying for jobs in industry.

    Personnellement, un mec comme ça, je le vire.

    Il a visiblement plus de connaissance du monde papier que du code réel.

    En php par exemple, le type Array ne représente pas du tout l’implémentation d’un array. Terme mal choisi ? Sans aucun doute. Raison d’écrire un article de 100 lignes et de faire chier ses élèves avec. Err…

    Au final, on s’en branle. En Python, on appelle “liste” le type de base qui regrouper et parcourir un ensemble ordonné d’éléments dont la taille n’est pas connue par avance mais qui est finie.

    bytearray et bytes sont bien des arrays par contre en Python, dans leurs implémentations et leurs api, bien que l’API soit plus riche que celle d’un array basique. Mais encore une fois, tu ne manipuleras presque jamais un array en Python. Seuls les programmeurs scientifiques ou de libs le feront. Dans la programmation de tous les jours, on utilise des listes, et c’est tout.

  • kontre

    @Sam: Le calcul numérique se fait en effet avec des arrays (c’est énormément plus rapide et plus efficace en mémoire), mais je ne pense pas que beaucoup de scientifiques utilisent des bytearray ou des bytes directement. Numpy a sa propre implémentation, qui a une API encore plus riche. Je sais que les données brutes sont stockées dans ce qu’ils appellent un buffer, mais je ne sais pas si ce buffer en question est un bytearray python.

  • herison

    Et le
    >>> import array

    dans tout ça ?

    Sinon pour les listes et les array si pour listes on pense à une implémentation par des listes chaînées

    Pour un tableau, l’accès au énième élément est directe pour une liste, il faut parcourir tout les éléments. Ca change des choses pour l’ajout et la suppression si l’élément est en plein milieu ou aux extrémités.

    En python pour avoir une liste (doublement) chainée il faut un collections.deque

    Merci pour ce site, c’est vraiment sympa.

  • Pi.0

    Ça me fait penser quand j’avais du apprendre le pascal et qu’il fallait mettre deux index dans l’array

  • Sam Post author

    @herison : array, c’est un module qui permet la création d’arrays contenant des types différents en Python. Arrays de float, de chars, de long, etc.

    Effectivement, on peut imaginer une liste chaînée, sauf que les structures de données Python sont toutes de haut niveau. Même les arrays du module array sont pas de simples arrays.

    Typiquement, une liste permet une collection d’objet hétérogènes. De plus, récupérer sa taille est une opération O(1) (https://wiki.python.org/moin/TimeComplexity). Donc les listes ne sont pas juste des arrays, il y a une couche par dessus.

    Bref, l’auteur veut juste à tout prix faire son kéké devant ses élèves. J’ai horreur de ces profs.

  • quetzal

    franchement, ce type byte est vriament une grosse galère… simplement a cause du carractère non-explicite de ce type de donnée…

    j’ai un machin pour lequel quand je fais, type var[0] (var en toute lettre) me renvoie un ‘int’ … le tout est encapsullé dans une b’list (remplie) de byte ecrite en toute lettre… (je crois que je vais mourir, c’est pire encore que l’encodage de python 2.7 unicode versus cp1258…)

    c’est par hasard que je me suis rendu compte que le type list, etait un type b’list (impossible d’afficher correctement les données, et pour les traduires, même avec .decode(utf-8) il me reste b” résiduel à l’affichage… bon je decouvre le type byte du a ce module (dont je tais le nom, parcequ’il existe (et déjà, c’est bô) donc

    mais comment je fait pour avoir ma petite liste remplie de gentil str bien sage, au lieu de ces int-byte-text(qui cache bien leur jeu …

    en mode explicite, le type byte ça serait pas plus simple ?? avec de vrai morceau de ‘int’ dedans, ou du x09 partout au lieu de faire croire au blaireau (dont je suis l’humble représentant) qu’ils sont de brave str gentiment exploitable…

    bref, après l’encodage unicode résolu, y’a un gars chez python qu’a pas pu s’empêcher de rendre tout b’ super klèr’ len=1, type =int.. grrrrrrrr

  • Sam Post author

    En Python, quand on récupère un bout de strings, on récupère une string plus petite. Bref, pour faciliter la vie des devs, s[0] ne donne pas une partie de s, mais une nouvelle séquence, de longueur 1.

    Comme les bytes sont beaucoup plus bas niveau, les bytes n’ont pas ce traitement magique, et récupérer un bout du type bytes() donne bien un seul des éléments qui compose ce flux d’octets. Un octest, c’est un nombre, donc c’est logique que ce soit un int.

    Attention, il N’Y A AUCUNE LETTRE dans les bytes.

    Je sais les gens ont une forte tendance à confondre ce qui s’affiche dans le shell avec les données elles-mêmes.

    Par exemple:

    >>> bytes([115, 97, 109])
        b'sam'

    Là on a une instruction qui dit : “créer un flux de données qui contient 3 octests. Je fournis ces valeurs sous la forme d’entiers, créés à partir d’une notation en base 10.

    Le shell me répond: “voilà, j’ai créé ton flux de donnée. je t’affiche une réprésentation de ce flux sous forme de caractères ascii”.

    La donnée n’est pas différent en entrée et en sortie. J’ai juste une réprésentation différente à la saisie et à l’affichage.

    Exemple, je fais:

    >>> bytes((0x73, 0x61, 0x6d))
        b'sam'

    Là on a AUSSI une instruction qui dit : “créer un flux de données qui contient 3 octests . Je fournis ces valeurs sous la forme d’entiers”. Mais j’ai créés ces entiers à partir d’une notation en base 10.

    Ensuite:

    >>> b'sam'
        b'sam'

    Là on a AUSSI une instruction qui dit : “créer un flux de données qui contient 3 octests . Je fournis ces valeurs sous la forme d’entiers”. Mais j’ai créés ces entiers à partir d’une notation en base ASCII.

    Et c’est la la truc : dans le monde du bas niveau, on manipule des octets (des bytes), donc des nombres, avec des représentations différentes. Par exemple avec des lettres ASCII. Mais c’est le même nombre.

    Par ailleurs, un autre difficulté et que la notation pour créer une lettre est très similaire à celle utilisée pour créer un octet:

    >>> 'a'
        'a'
     
    >>> b'a'
        b'a'

    Mais cest instruction ne font pas DU TOUT la même chose. L’un créé un type destiné à être manipulé comme des lettres. L’autre créé un type destiné à être manipulé comme une sequence de chiffres.

    Donc, au contraire, le type bytes est TRES explicite, et force les programmeurs à clairement délimiter les cas où ils manipules des données qui sont conceptuellement des octets bas niveau, celles qui sont conceptuellement du texte.

    Je dis conceptuellement, car évidement, au niveau de l’implémentation, le texte est représenté par des octets. Mais quand on manipule du texte, on est pas intéressé par sa valeur en tant qu’octet. On veut l’afficher. Le splitter. Le mettre en majuscule.

    Quand on manipule des bytes, on est intéressé par la valeur des octets.

    Alors vient une difficulté supplémentaire: comment afficher les bytes qu’on a manipulé. Et bien il faut le concertir explictement en type str. Car cela oblige à penser à l’encoding et à l’échappement, une grosse source de bug qui pose problèmes à tout ceux qui manipulent bytes et textes sont faire attention.

  • lechevalier denis

    Bonjour

    J’ai trouvé un script sur le net qui utilise le module bytearray pour la recherche de nombres premiers . Mais quand je lance le script python3 me dit que bytearray n’a pas l’attribut setall .

    Merci à l’avance

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.