Les E/S du fichier Python sur Windows et Linux sont deux choses différentes.
Lorsque vous utilisez des threads, vous pouvez rencontrer des problèmes causés par le comportement différent des fonctions d'entrée/sortie de fichiers.

J'ai un programme Python qui fonctionne bien sur Linux. Il y a quelques mois, j'ai voulu le faire fonctionner sur Windows.
C'était la première fois que j'utilisais Python sur Windows. Installer l'application Python , créer virtual environment, copier et exécuter. Aucun problème ... oh mais il y avait un problème. Ma session disparaissait parfois ... WTF ! J'ai remarqué le problème en appuyant plusieurs fois sur F5 dans un laps de temps très court. Il est temps de procéder à une enquête approfondie.
L'application et le magasin de valeurs clés de la session
L'application Python est une application Flask . Elle utilise l'interface de système de fichiers de Flask-Session pour stocker les sessions sous forme de fichiers. Flask-Session utilise un autre paquet PyPi pour gérer toutes les entrées/sorties de fichiers. Comme vous pouvez l'imaginer, ce paquetage implémente un magasin de valeurs clés.
Il y a deux méthodes principales dans ce paquetage :
- set(key, value), pour écrire/mettre à jour les données de la session
- get(key), pour récupérer les données de la session.
Dans le paquetage, la méthode set(key, value) crée un fichier temporaire, puis appelle la fonction Python os.replace() pour remplacer le fichier de session. La méthode get(key) appelle la fonction read de Python .
Test à l'aide de threads
Lorsque vous exécutez Flask en mode développement, vous verrez beaucoup de demandes au magasin de sessions car Flask sert également images, des fichiers CSS, etc. Dans un environnement de production où vous servez du contenu statique via un serveur web, vous avez moins de chances de rencontrer ce problème, mais il est toujours présent !
C'est ainsi que m'est venue l'idée d'écrire un petit test utilisant des threads.
import cachelib
import threading
fsc = cachelib.file.FileSystemCache('.')
def set_get(i):
fsc.set('key', 'val')
val = fsc.get('key')
for i in range(10):
t = threading.Thread(target=set_get, args=(i,))
t.start()
Sur Linux , il n'y avait aucune erreur, rien. Mais sur Windows cela a soulevé des exceptions générées aléatoirement :
[WinError 5] Access is denied
...
[Errno 13] Permission denied
L'exception [WinError 5] a été générée par la fonction os.replace() de Python . L'exception [Errno 13] a été générée par la fonction read() de Python .
Que se passe-t-il ici ?
Les E/S de fichiers sur Windows et Linux sont deux choses différentes.
Je pensais que la Python me protégerait des implémentations spécifiques aux plateformes. C'est le cas dans de nombreuses fonctions, mais pas dans toutes. En particulier, lorsque vous utilisez des threads, vous pouvez rencontrer des problèmes causés par le comportement différent des fonctions d'entrée/sortie de fichiers.
Extrait de Python Bug Tracker Issue46003 :
Comme on dit, il n'y a pas de "logiciel portable", seulement des "logiciels qui ont été portés". Surtout dans un domaine comme les E/S de fichiers : dès que l'on va au-delà du simple "un processus ouvre, écrit et ferme" et qu'un autre processus "ouvre, lit et ferme", il y a beaucoup de problèmes spécifiques à la plate-forme. Python n'essaie pas de faire abstraction de tous les problèmes possibles d'E/S de fichiers. |
Fonction read() de Python
Dans la bibliothèque que j'utilise, la méthode get(key) utilise la fonction Python read().
Sur Linux il suffit de mettre la fonction read() dans un try-except :
try:
with open(f, 'r') as fo:
return fo.read()
except Exception as e:
return None
La fonction attendra que les données soient disponibles. Une exception ne sera levée qu'en cas de dépassement du délai d'attente, qui est de 60 secondes sur la plupart des systèmes Linux , ou en cas d'erreur inattendue.
Sur Windows , l'échec sera immédiat si le fichier est accédé par un autre thread. Pour créer le même comportement qu'avec la Linux , nous devons ajouter des tentatives et un délai, par exemple :
max_sleep_time = 10
total_sleep_time = 0
sleep_time = 0.02
while total_sleep_time < max_sleep_time:
try:
with open(f, 'r') as fo:
return fo.read()
except OSError as e:
errno = getattr(e, 'errno', None)
if errno == 13:
# permission error
time.sleep(sleep_time)
total_sleep_time += sleep_time
sleep_time *= 2
else:
# some other error
return None
except Exception as e:
return None
# out of retries
return None
Fonction Python os.replace()
Dans la bibliothèque que j'utilise, la méthode set(key, value) utilise la fonction Python os.replace().
Sur Linux il suffit de mettre la fonction os.replace() dans un try-except :
try:
os.replace(src, dst)
return True
except Exception as e:
return False
La fonction attendra que le fichier puisse être remplacé. Une exception ne sera levée qu'en cas de dépassement du délai d'attente, qui est de 60 secondes sur la plupart des systèmes Linux , ou en cas d'erreur inattendue.
Sur Windows , l'échec sera immédiat si le fichier est accédé par un autre thread. Pour créer le même comportement que nous avons avec Linux , nous devons ajouter des tentatives et un délai, par exemple :
max_sleep_time = 10
total_sleep_time = 0
sleep_time = 0.02
while total_sleep_time < max_sleep_time:
try:
os.replace(src, dst)
return True
except Exception as e:
winerror = getattr(e, 'winerror', None)
if winerror == 5:
time.sleep(sleep_time)
total_sleep_time += sleep_time
sleep_time *= 2
else:
# some other error
return False
# out of retries
return False
Conclusion
Créer un programme Python qui peut fonctionner sur plusieurs plateformes peut être compliqué car vous pouvez rencontrer des problèmes comme ceux décrits ci-dessus. Au début, j'ai été surpris que Python ne me cache pas la complexité de Windows . Venant de Linux je me suis dit, Python pourquoi ne pas faire fonctionner cela sur Windows de la même manière que sur Linux ?
Mais c'est le choix qu'ont fait les développeurs de Python . Il se peut que ce ne soit pas possible non plus. Je n'ai pas trouvé une seule ligne dans les docs Python en ligne qui m'ait alerté et j'ai remarqué que beaucoup de gens ont du mal avec ça. J'ai soumis un rapport de bogue pour demander qu'un avertissement soit ajouté pour les développeurs lorsqu'ils développent pour plusieurs plateformes. Mais je me suis abstenu de le faire par la suite car je me rends compte que je suis très partial, venant de Linux.
Liens / crédits
backports.py
https://github.com/flennerhag/mlens/blob/master/mlens/externals/joblib/backports.py
os.replace
https://docs.python.org/3/library/os.html?highlight=os%20replace#os.replace
os.replace is not cross-platform: at least improve documentation
https://bugs.python.org/issue46003
En savoir plus...
Threads
Récent
- Collecter et bloquer IP addresses avec ipset et Python
- Comment annuler des tâches avec Python Asynchronous IO (AsyncIO)
- Exécuter une commande Docker dans un conteneur Cron Docker
- Création d'un Captcha avec Flask, WTForms, SQLAlchemy, SQLite
- Multiprocessing, verrouillage des fichiers, SQLite et tests
- Envoi de messages à Slack à l'aide de chat_postMessage
Les plus consultés
- Flask RESTful API validation des paramètres de la requête avec les schémas Marshmallow
- Utiliser UUIDs au lieu de Integer Autoincrement Primary Keys avec SQLAlchemy et MariaDb
- Utilisation des Python's pyOpenSSL pour vérifier les certificats SSL téléchargés d'un hôte
- Connexion à un service sur un hôte Docker à partir d'un conteneur Docker
- Utiliser PyInstaller et Cython pour créer un exécutable Python
- SQLAlchemy : Utilisation de Cascade Deletes pour supprimer des objets connexes