Architecture des ordinateurs TP-2

by Joseph Razik, last modified on 2023-03-23

2   Codage et fichiers binaires (partie 2)

2.1   Objectif

L'objectif de ce TP est de manipuler les différentes formes de représentation de valeurs (nombres entiers, réels, caractères, textes) avec le langage Python. Par exemple, à la fin, vous manipulerez une image et du son que vous afficherez.

2.2   Rappel

Dans le répertoire /home/partage/I22/ se trouve un fichier valeurs contenant une succession d'objets de différents types, au format dit binaire , dont dans cet ordre :

  • la valeur 192, nombre entier non signé codé sur 8 bits
  • la valeur -1, nombre entier signé codé sur 8 bits,
  • la valeur -2, nombre entier signé codé sur 16 bits,
  • la valeur 1633837924, nombre entier non signé codé sur 32 bits,
  • la valeur 1.0, nombre réel codé au format IEEE 754 32 bits (nombre à virgule flottante),
  • la valeur -10.25, nombre réel codé au format IEEE 754 64 bits (nombre à virgule flottante),

2.3   Fichiers en Python

  • Placez-vous avec le terminal dans votre répertoire I22 et exécutez l'interprète Python avec la commande ipython3,
  • lisez l'aide de la commande open pour savoir comment l'utiliser en saisissant dans l'interprète help(open).

Pour manipuler un fichier en Python, que ce soit pour lire son contenu, le créer ou y écrire, il faut avant tout utiliser l'instruction open(). Cette instruction vous retournera un objet particulier appelé descripteur de fichier qui vous permettra de le manipuler. Une fois les opérations d'écriture et de lecture terminées, il faut refermer le fichier et libérer le descripteur à l'aide de l'instruction close(). Certaines opérations ne se feront effectivement qu'à cette occasion, notamment le vidage des tampons d'écriture à cause du processus de bufferisation. Ce processus de bufferisation permet de stoc ker dans une espace intermédiaire (appelé tampon) les données à écrire. Une fois cette espace suffisamment plein, les données sont effectivement écrites en une fois sur le disque. Ce processus permet d'optimiser les entrées/sorties qui sont des opérations très coûteuses en temps.

Dans vos programmes, vous aurez donc un code ressemblant à ceci :

# Ouverture et fermeture explicite
f = open('nom_du_fichier')

# votre code utilisant le fichier

# fermeture du fichier
f.close()

2.4   Lecture d'un fichier texte

Pour un fichier dont f est le descripteur, Python possède plusieurs fonctions pour lire le contenu d'un fichier :

  • f.read(n)  : lit au plus n caractères ;
  • f.readline()  : lit une ligne entière, délimitée par le caractère spécial de fin de ligne \n (inclus) ;
  • f.readlines()  : lit tout le fichier d'un coup et retourne un tableau formé de chaque ligne du fichier.

Lors de la manipulation d'un fichier, le système gère en fait la position en octet d'un curseur dans ce fichier, qui avance proportionnellement au nombre d'octets lus ou écrits. À l'ouverture, le curseur est positionné au début du fichier (premier octet), à la lecture ou l'écriture d'un octet le curseur se déplace d'un octet. Les différents formats d'encodage des chaînes de caractères pouvant utiliser un nombre différent d'octets pour encoder un caractère, le nombre d'octets de déplacement à la lecture d'un caractène va donc dépendre de cet encodage utilisé pour le texte. La position du curseur n'est pas réinitialisée après chaque opération de lecture ou d'écriture, elle ne fait qu'augmenter. Ainsi, en mode texte, mode par défaut, la succession d'instructions read(1) ; read(1) va lire deux caractères successifs car à chaque opération read(1) le curseur de position dans le fichier avance d'un caractère.

  1. En ouvrant le fichier qu'une fois, affichez les 12 premiers caractères du fichier texte1 en utilisant au moins 2 instructions read() de suite  ;
  2. En ouvrant le fichier qu'une fois, affichez la première et la troisième ligne du même fichier en utilisant l'instruction readline()  ;
  3. En ouvrant le fichier qu'une fois, affichez la seconde et quatrième ligne du même fichier en utilisant l'instruction readlines() (attention au s à la fin de readlines()).

Toutefois, il existe des instructions pour manipuler directement la position du curseur sans avoir à fermer puis ré-ouvrir le fichier. Mais attention, ces commandes manipule la position du curseur en octets et non en caractères.

  • tell()  : retourne la position courante (en octets) du curseur dans le fichier ;
  • seek()  : permet de placer le curseur à une position précise (en octets) dans le fichier.

Par défaut, seek() prend pour origine de placement le début du fichier, seule origine valide en mode texte.

Dans la même suite d'instructions, sans refermer le fichier entre chaque opération, réalisez les actions suivantes :

  1. Lisez et affichez les trois premières lignes du fichier texte1 ainsi que la position du curseur,
  2. Relisez et affichez les trois premiers caractères du même fichier et la position du curseur.

2.5   Écriture d'un fichier texte

Un des paramètres de la fonction open() est le mode. Celui-ci permet d'indiquer le mode d'ouverture voulu pour le fichier :

  • 'r'  : mode lecture (valeur par défaut),
  • 'w'  : mode écriture, écrit dès le début du fichier et écrase le contenu du fichier s'il existe,
  • 'a'  : mode ajout, écrit à la fin du fichier (n'écrase pas le contenu).

Une fois le fichier ouvert en mode écriture, il est possible de modifier le contenu du fichier. S'il n'est ouvert qu'en mode lecture, une tentative d'écriture entraînera une erreur. Par ailleurs, l'obligation de passer par l'instruction open() pour manipuler un fichier permet au système de s'assurer qu'il n'est ouvert en écriture que par un seul et unique programme. En effet, la modification simultanée d'un même fichier par deux processus différents entraînerait une incohérence dans les données.

L'instruction pour écrire des caractères dans un fichier est la commande write(). Cette fonction retourne le nombre de caractères qui ont été écrits. Cette instruction écrit le texte qu'on lui donne en paramètre, qu'il fasse ou non plusieurs lignes. Le curseur de position dans le fichier est augmenté du nombre d'octets correspondant au nombre de caractères écrits.

  1. Directement à partir de l'interprète Python (sans écrire de programme), créez sans le refermer un fichier nommé fichier_test dans votre répertoire de travail I22 et écrivez dans celui-ci le texte suivant : « La cigale et la fourmi » .
  2. À partir d'un autre terminal, affichez avec cat ou less le contenu du fichier que venez de créer. Tout s'est-il bien passé ?
  3. Ajoutez à la suite de ce fichier, sur une nouvelle ligne, le texte « Le scorpion et la grenouille ».
  4. Toujours à partir de l'autre terminal, vérifiez que le contenu du fichier correspond.
  5. Refermez le fichier dans l'interprète et vérifiez maintenant le contenu du fichier.

2.6   Lecture/écriture binaire

  1. En écrivant un programme Python, lisez et affichez les 4 premiers caractères du fichier valeurs. Retrouvez-vous bien les valeurs attendues, comme avec od  ? Pourquoi  ?
  2. Toujours en Python, créez un nouveau fichier fichier_test_bis dans votre répertoire de travail et écrivez dedans les nombres 1337, -2 et 10.25.
  3. À partir d'un autre terminal, vérifiez le contenu du fichier. Retrouvez-vous la même chose qu'en utilisant la commande od comme en début de sujet ?

L'explication est que par défaut, le mode de lecture et d'écriture est dit textuel. Ainsi les fonctions read() et write() ne traitent dans ce contexte que des chaînes de caractères. Or, dans le fichier valeurs, les données ne sont pas représentées par leur chaîne de caractères mais pas un codage binaire particulier (entiers signés, virgule flottante 32 bits, etc). Pour traiter ce cas, il faut ouvrir le fichier en mode dit binaire, avec l'option 'b'. Ainsi une ouverture en lecture utilisera le mode 'rb' et une écriture utilisera le mode 'wb'.

En mode binaire, les instructions read() et write() ne traitent plus le type de base str (chaîne de caractères) mais le type de base, bytes, c'est-à-dire une suite d'octets. En effet, l'ordinateur ne voit qu'une succession d'octets sans sens.

Pour donner un sens à cette suite d'octets, il faut traduire celle-ci en quelque chose de compréhensible à partir d'un format d'interprétation. Il y a deux possibilités :

  • Conversion de ou vers une chaîne de caractères avec un encodage particulier : utilisez les fonctions encode() et decode()
# Conversion byte vers str
chaine.decode('UTF-8')
print(chaine)

# Conversion str vers byte
chaine.encode('UTF-8')
  • Conversion de ou vers des codages de nombres (entiers ou à virgule flottante) : utilisez les fonctions pack() et unpack() du module struct.
import struct
# Conversion byte vers un nombre
nombre = struct.unpack('<b', octet)[0]

# Conversion nombre vers byte
octet = struct.pack('<b', nombre)

Les formats définis et reconnus par struct sont écrits dans l'aide du module dont voici un extrait :

The optional first format char indicates byte order, size and alignment:
   @: native order, size & alignment (default)
   =: native order, std. size & alignment
   <: little-endian, std. size & alignment
   >: big-endian, std. size & alignment
   !: same as >

 The remaining chars indicate types of args and must match exactly;
 these can be preceded by a decimal repeat count:

 +--------+----------------+--------------+
 | Format | C Type         |Standard size |
 +========+================+==============+
 | c      | char           | 1            |
 +--------+----------------+--------------+
 | b      | signed char    | 1            |
 +--------+----------------+--------------+
 | B      | unsigned char  | 1            |
 +--------+----------------+--------------+
 | h      | short          | 2            |
 +--------+----------------+--------------+
 | H      | unsigned short | 2            |
 +--------+----------------+--------------+
 | i      | int            | 4            |
 +--------+----------------+--------------+
 | I      | unsigned int   | 4            |
 +--------+----------------+--------------+
 | f      | float          | 4            |
 +--------+----------------+--------------+
 | d      | double         | 8            |
 +--------+----------------+--------------+
 | s      | char[] string  |              |
 +--------+----------------+--------------+

Special cases (preceding decimal count indicates length):
   s:string (array of char); p: pascal string (with count byte).
Whitespace between formats is ignored.
  1. À partir de ces informations, écrivez le code Python permettant d'afficher les 6 premières valeurs contenues dans le fichier valeurs comme rappelé au début.
  2. Créez le fichier fichier_test_bin dans votre répertoire de travail, dans lequel vous écrirez les nombres 1337 (entier non signé codé sur 2 octets), -2 (entier signé sur 4 octets) et 10.25 (nombre à virgule flottante codé sur 4 octets).
  3. À l'aide de la commande od et des options adéquates, vérifiez que le contenu du fichier correspond bien à votre suite de valeurs attendues, en mode binaire.

2.7   Mise en pratique

À partir de tout ce que vous avez vu dans ce TP, vous allez écrire en Python quelques fonctions qui vont manipuler des données textuelles ou binaires.

  1. Écrivez la fonction majuscule() qui prend en paramètre une chaîne de caractères et qui retourne la même chaîne mais en majuscule. Pour cela vous utiliserez les fonctions ord() et chr() et l'astuce additive pour passer des minuscules aux majuscules et inversement. Vous ne convertirez que les 26 lettres de l'alphabet, pas les lettres accentuées ou autres caractères spéciaux et évidemment vous n'utiliserez pas la fonction upper(). Exemple :

    >>> majuscule('Salut ! Ça va ?')
    'SALUT ! ÇA VA ?'
    
  2. Écrivez la fonction val2ascii() qui prend en paramètre un nombre entier positif et retourne sa représentation en chaîne de caractères à l'aide d'une astuce additive, c'est-à-dire une fonction équivalente à str() (donc évidemment sans utiliser la fonction str()). Exemple :

    >>> val2ascii(123)
    '123'
    
  3. Le fichier /home/partage/I22/data_1 contient une séquence de 512x512 nombres entiers codés sur un octet non signé. Écrivez la fonction affiche2D() qui prend en paramètre un nom de fichier et affiche son contenu sous forme d'image à l'aide des commande suivantes :

    import matplotlib.pyplot as plt
    
    # Soit X un tableau à deux dimensions
    X = [[1, 1, 1, 2], [2, 1, 2, 2]]
    # l'instruction suivante calcule l'image en niveau de gris
    plt.imshow(X, cmap=plt.cm.gray)
    # l'instruction suivante l'affiche
    plt.show()
    
  4. Le fichier /home/partage/I22/data_2 contient une double séquence de nombres entiers signés codés sur 2 octets. Chaque nombre appartient alternativement à la séquence G ou à la séquence D. Écrivez la fonction affiche_2_canaux() qui prend en paramètre le nom d'un fichier et affiche séparément les deux séquences de nombres sous forme de deux graphiques à l'aide des commande suivantes :

    import matplotlib.pyplot as plt
    
    # Soit Y un tableau à une dimension, les valeurs successives à afficher
    Y = [1, -1, 1.5, 0.8, -0.5]
    # on déclare une nouvelle figure
    plt.figure()
    # on calcule l'image
    plt.plot(Y)
    # on affiche l'image
    plt.show()