Licence CC BY-NC-ND, Thierry Parmentelat & Arnaud Legout

from IPython.display import HTML
HTML(filename="_static/style.html")

fichiers#

  • on a très souvent besoin de lire ou d’écrire un fichier par programme
    notamment bien sûr pour lire les entrées ou sauver les résultats

  • pour la lecture en pratique on utilise souvent des librairies
    e.g. pour lire du json ou du yaml, ou des formats spécialisés

  • toutefois il est bon de savoir utiliser les outils de bas niveau
    (enfin, aussi bas niveau que ce qu’offre Python…)

formats et librairies#

si vous devez lire des formats communs, faites-le avec:

  • le module standard json pour du JSON

  • la librairie externe PyYAML pour du YAML

  • pandas.read_csv() pour du csv

  • etc…

mais sinon: open() est le point d’entrée#

  • lire et écrire un fichier est très facile en Python

  • ouvrir un fichier pour créer un objet “fichier”

  • open('mon_fichier.txt', 'r')

    • 'r' ouvre le fichier en lecture (défaut),

    • ‘w’ en écriture,

    • ‘a’ en écriture à la suite (append),

  • open() retourne un objet de type fichier

  • qu’il faut bien penser à refermer
    sans quoi on provoque des fuites de file descriptors, et au bout
    d’un moment l’OS ne nous laisse plus ouvrir de fichiers du tout

utilisez toujours un with#

  • c’est pourquoi il est recommandé

  • de prendre l’habitude de toujours utiliser un context manager

# on n'a pas encore étudié l'instruction with
# mais je vous conseille de toujours procéder comme ceci

# avec with on n'a pas besoin de fermer le fichier
with open('temporaire.txt', 'w') as writer:
    for i in 10, 20, 30:
        print(f"{i} {i**2}", file=writer)

avantage du with:

  • pas besoin de fermer

  • même en cas de gros souci (exception)

lecture avec for#

  • l’objet fichier est un itérable lui-même

  • donc on peut l’utiliser dans un for pour traiter une ligne à la fois

  • c’est *la méthode recommandée pour lire un fichier texte

avec un newline

attention toutefois, car chaque ligne va contenir un caractère "\n" de fin de ligne (sauf éventuellement la dernière)

# pour inspecter ce qu'on vient d'écrire
# dans le fichier qui s'appelle "temporaire.txt"
# dans le répertoire courant

# lire un fichier texte ligne par ligne
# on ne peut pas faire plus compact et lisible !

# remarquez aussi:
# open() sans le mode ⇔ open('r')

with open("temporaire.txt") as reader:
    for line in reader:
        # attention ici line contient déjà le newline
        # c'est pourquoi on demande à print() de ne pas
        # en ajouter un second
        print(line, end="")
10 100
20 400
30 900

on peut régler le comportement de print()

sans réglage particulier, la fonction print() écrit dans le terminal (ou le notebook), et ajoute automatiquement une fin de ligne
on peut agir sur ce comportement avec

  • print(..., file=writer) comme on l’a fait au tout début de ce notebook pour créer le fichier temporaire.txt

  • print(..., end="") comme on vient de le faire pour éviter le saut de ligne automatique

autres méthodes en lecture#

comme toujours, il y a plein d’autres méthodes disponibles sur les fichiers texte, reportez-vous à la documentation pour des besoins spécifiques

fichiers ouverts en binaire#

ajouter b dans le mode#

tous les fichiers ne sont pas des fichiers texte
par ex. un exécutable, un fichier dans un format propriétaire

pour ouvrir un fichier en mode binaire:

  • on ajoute b au mode d’ouverture,

  • on interagit avec l’objet fichier avec un objet bytes et non str

  • c’est-à-dire que la lecture retourne un bytes
    et on ne peut que écrire un bytes

  • il n’y a aucun encodage, décodage,
    et aucune conversion de fin de ligne (auberge espagnole)

# pour fabriquer un objet bytes, je peux par exemple 
# encoder un texte qui comporte des accents
# (on reparlera des encodages plus tard)

text = "noël en été\n"
binaire = text.encode(encoding="utf-8")

binaire
b'no\xc3\xabl en \xc3\xa9t\xc3\xa9\n'
# j'ai bien un objet bytes, 
# et sa taille correspond au nombre d'octets 
# et non pas au nombre de caractères

type(binaire), len(binaire), len(text)
(bytes, 15, 12)
# remarquez le 'b' dans le mode d'ouverture

with open('temporaire.bin', 'wb') as out_file:
    # je peux du coup écrire un objet bytes
    out_file.write(binaire)
# pareil en lecture, le mode avec un 'b'
# va faire que read() retourne un objet bytes

with open('temporaire.bin', 'rb') as in_file:
    binaire2 = in_file.read()
# et donc on retombe bien sur nos pieds
binaire2 == binaire
True
# ça aurait été pareil 
# si on avait ouvert le fichier en mode texte
# puisque ce qu'on a écrit dans le fichier binaire,
# c'est justement l'encodage en utf-8 d'un texte

#             sans le b ici ↓↓↓
with open('temporaire.bin', 'r') as feed:
    text2 = feed.read()
    
text2 == text
True

le module pathlib#

objectifs#

  • simplifier la gestion des noms de fichier

    • pour rendre le code plus concis, et donc plus lisible

  • notamment, en remplacement de os.path qui est old-school (et super vilain)

  • le sous-titre pour pathlib: object-oriented filesystem paths

présentation du module#

  • orienté objet

  • examiner le contenu du disque existe ou pas, quel type, globbing

  • les calculs sur les noms de fichiers
    concaténation, suffixes, remonter dans l’arbre …

  • métadonnées
    taille, dates de modification, …

  • permet d’ouvrir les fichiers

  • ne gère pas les urls

  • voir documentation complète

un exemple#

  • orienté objet

  • le sujet devient plus visible

  • NB: un objet Path est immutable

# savoir si un chemin correspond à un dossier
from pathlib import Path

tmp = Path("temporaire.txt")

if tmp.is_file():
    print("c'est un fichier")
c'est un fichier
# donc on peut l'ouvrir

with tmp.open() as feed:
    for line in feed:
        print(line, end="")
10 100
20 400
30 900
# on peut faire des calculs sur les noms de fichier
tmp.suffix
'.txt'
# changer de suffixe
tmp.with_suffix(".py")
PosixPath('temporaire.py')
# calculer le chemin absolu
# le couper en morceaux, etc...
tmp.absolute().parts
('/',
 'home',
 'docs',
 'checkouts',
 'readthedocs.org',
 'user_builds',
 'ue12-p24-python',
 'checkouts',
 'p24',
 'notebooks',
 'temporaire.txt')

construire un objet Path#

# un chemin absolu
prefix = Path("/etc")

# le chemin absolu du directory courant
dot = Path.cwd()

# ou du homedir
home = Path.home()

# un nom de ficher
filename = Path("apache")
# par exemple le répertoire courant est

dot
PosixPath('/home/docs/checkouts/readthedocs.org/user_builds/ue12-p24-python/checkouts/p24/notebooks')

l’opérateur /#

un exemple intéressant de surcharge d’opérateur: selon le type de ses opérandes, / fait .. ce qu’il faut ! par exemple ici on ne fait pas une division !

# Path / Path -> Path bien sûr
type(prefix / filename)
pathlib.PosixPath
# Path / str -> Path
type(prefix / "apache2")
pathlib.PosixPath
# str / Path -> Path
type("/etc" / Path("apache2"))
pathlib.PosixPath
# mais bien sûr str / str -> TypeError
try:
    "/etc" / "apache2"
except Exception as e:
    print("OOPS", e)
OOPS unsupported operand type(s) for /: 'str' and 'str'

calculs sur les chemins#

j’ai créé un petite hiérarchie de fichiers dans le dossier filepath-globbing qui ressemble à ceci

_images/filepath-globbing.png
# pour commencer, voilà comment on peut trouver son chemin absolu

globbing = Path("filepath-globbing")
absolute = globbing.absolute()
absolute
PosixPath('/home/docs/checkouts/readthedocs.org/user_builds/ue12-p24-python/checkouts/p24/notebooks/filepath-globbing')
# si on a besoin d'un str, comme toujours il suffit de faire

str(absolute)
'/home/docs/checkouts/readthedocs.org/user_builds/ue12-p24-python/checkouts/p24/notebooks/filepath-globbing'
# les différents morceaux de ce chemin absolu

absolute.parts
('/',
 'home',
 'docs',
 'checkouts',
 'readthedocs.org',
 'user_builds',
 'ue12-p24-python',
 'checkouts',
 'p24',
 'notebooks',
 'filepath-globbing')
# juste le nom du fichier, sans le chemin

absolute.name
'filepath-globbing'
# le chemin, sans le nom du fichier

absolute.parent
PosixPath('/home/docs/checkouts/readthedocs.org/user_builds/ue12-p24-python/checkouts/p24/notebooks')
# tous les dossiers parent

list(absolute.parents)
[PosixPath('/home/docs/checkouts/readthedocs.org/user_builds/ue12-p24-python/checkouts/p24/notebooks'),
 PosixPath('/home/docs/checkouts/readthedocs.org/user_builds/ue12-p24-python/checkouts/p24'),
 PosixPath('/home/docs/checkouts/readthedocs.org/user_builds/ue12-p24-python/checkouts'),
 PosixPath('/home/docs/checkouts/readthedocs.org/user_builds/ue12-p24-python'),
 PosixPath('/home/docs/checkouts/readthedocs.org/user_builds'),
 PosixPath('/home/docs/checkouts/readthedocs.org'),
 PosixPath('/home/docs/checkouts'),
 PosixPath('/home/docs'),
 PosixPath('/home'),
 PosixPath('/')]

pattern-matching#

# est-ce que le nom de mon objet Path 
# a une certaine forme ?

absolute.match("**/notebooks/*")
True
absolute.match("**/*globbing*")
True

pattern-matching - dans un dossier#

_images/filepath-globbing.png
# à présent c'est plus intéressant
# avec des chemins relatifs
root = Path("filepath-globbing")

# tous les fichiers / répertoires 
# qui sont immédiatement dans le dossier
list(root.glob("*"))
[PosixPath('filepath-globbing/a1'),
 PosixPath('filepath-globbing/c'),
 PosixPath('filepath-globbing/b')]
# les fichiers/dossiers immédiatement dans le dossier
# et dont le nom se termine par un chiffre
list(root.glob("*[0-9]"))
[PosixPath('filepath-globbing/a1')]

pattern-matching - dans tout l’arbre#

_images/filepath-globbing.png
# ce dossier, et les dossiers en dessous
# à n'importe quel étage

list(root.glob("**"))
[PosixPath('filepath-globbing'),
 PosixPath('filepath-globbing/c'),
 PosixPath('filepath-globbing/b'),
 PosixPath('filepath-globbing/c/cc')]
# tous les fichiers/dossiers 
# dont le nom termine par un chiffre

list(root.glob("**/*[0-9]"))
[PosixPath('filepath-globbing/a1'),
 PosixPath('filepath-globbing/b/b2'),
 PosixPath('filepath-globbing/c/cc/c2')]

notions avancées#

encodages par défaut#

  • vous remarquez qu’on a souvent appelé open() sans préciser l’encodage

  • l’encodage par défaut pour un fichier ouvert en mode texte est celui retourné par

import locale
locale.getpreferredencoding(False)
'UTF-8'
  • appeler open() sans préciser l’encodage peut être risqué

    • dépend des réglages sur la machine cible

  • il vaut mieux toujours être explicite et préciser l’encodage

with open('temporaire.txt', 'r', encoding='utf8') as in_file:
    print(in_file.read())
10 100
20 400
30 900
  • le problème est toutefois de moins en moins aigü

    • Windows, MacOS et Linux à présent configurés par défaut pour UTF-8

  • si vous avez encore du cp1252 (vieux Windows) ou des ISO-latin15 (Unix)

    • je vous recommande de transcoder tout ça !

fichiers système#

  • sys.stdout, sys.stdin, sys.stderr

    • sortie, entrée et erreur standard

    • accessibles donc au travers du module sys

import sys
sys.stdout
<ipykernel.iostream.OutStream at 0x7f3a0ffca350>