web-dev-qa-db-fra.com

Quelle est la meilleure façon d'ouvrir un fichier pour un accès exclusif en Python?

Quelle est la façon la plus élégante de résoudre ce problème:

  • ouvrir un fichier en lecture, mais uniquement s'il n'est pas déjà ouvert en écriture
  • ouvrir un fichier pour l'écriture, mais seulement s'il n'est pas déjà ouvert pour la lecture ou l'écriture

Les fonctions intégrées fonctionnent comme ceci

>>> path = r"c:\scr.txt"
>>> file1 = open(path, "w")
>>> print file1
<open file 'c:\scr.txt', mode 'w' at 0x019F88D8>
>>> file2 = open(path, "w")
>>> print file2
<open file 'c:\scr.txt', mode 'w' at 0x02332188>
>>> file1.write("111")
>>> file2.write("222")
>>> file1.close()

scr.txt contient maintenant '111'.

>>> file2.close()

scr.txt a été remplacé et contient désormais '222' (sous Windows, Python 2.4).

La solution doit fonctionner à l'intérieur du même processus (comme dans l'exemple ci-dessus) ainsi que lorsqu'un autre processus a ouvert le fichier.
Il est préférable, si un programme en panne ne garde pas le verrou ouvert.

43
mar10

Je ne pense pas qu'il existe une méthode entièrement multiplateforme. Sous Unix, le module fcntl le fera pour vous. Cependant sur les fenêtres (que je suppose que vous êtes par les chemins), vous devrez utiliser le module win32file.

Heureusement, il existe une implémentation portable ( portalocker ) utilisant la méthode appropriée de la plate-forme dans le livre de recettes python.

Pour l'utiliser, ouvrez le fichier, puis appelez:

portalocker.lock(file, flags)

où les indicateurs sont portalocker.LOCK_EX pour un accès en écriture exclusif ou LOCK_SH pour un accès en lecture partagé.

25
Brian

La solution doit fonctionner à l'intérieur du même processus (comme dans l'exemple ci-dessus) ainsi que lorsqu'un autre processus a ouvert le fichier.

Si par "un autre processus" vous voulez dire "quel que soit le processus" (c'est-à-dire pas votre programme), sous Linux, il n'y a aucun moyen d'y parvenir en se basant uniquement sur les appels système ( fcntl & copains). Ce que vous voulez c'est verrouillage obligatoire , et la manière Linux de l'obtenir est un peu plus compliquée:

Remontez la partition qui contient votre fichier avec l'option mand :

# mount -o remount,mand /dev/hdXY

Définissez l'indicateur sgid pour votre fichier:

# chmod g-x,g+s yourfile

Dans votre code Python, obtenez un verrou exclusif sur ce fichier:

fcntl.flock(fd, fcntl.LOCK_EX)

Désormais, même cat ne pourra pas lire le fichier tant que vous n'aurez pas relâché le verrou.

11

EDIT: Je l'ai résolu moi-même! En utilisant existence du répertoire et l'âge comme mécanisme de verrouillage! Le verrouillage par fichier n'est sûr que sous Windows (car Linux écrase silencieusement), mais le verrouillage par répertoire fonctionne parfaitement à la fois sous Linux et Windows. Voir mon GIT où j'ai créé une classe facile à utiliser 'lockbydir.DLock' pour cela:

https://github.com/drandreaskrueger/lockbydir

Au bas du fichier lisez-moi, vous trouverez 3 GITplayers où vous pouvez voir les exemples de code s'exécuter en direct dans votre navigateur! Assez cool, non? :-)

Merci de votre attention


C'était ma question initiale:

Je voudrais répondre à parity3 ( https://meta.stackoverflow.com/users/1454536/parity ) mais je ne peux ni commenter directement ('Vous devez avoir 50 points de réputation pour commenter'), ni puis-je voir un moyen de le contacter directement? Que me suggérez-vous pour le joindre?

Ma question:

J'ai implémenté quelque chose de similaire à ce que parity3 a suggéré ici comme réponse: https://stackoverflow.com/a/21444311/3693375 ("En supposant que votre interprète Python, et le ...")

Et cela fonctionne à merveille - sous Windows. (Je l'utilise pour implémenter un mécanisme de verrouillage qui fonctionne sur des processus démarrés indépendamment. https://github.com/drandreaskrueger/lockbyfile )

Mais à part la parité3, cela ne fonctionne PAS de la même manière sous Linux:

os.rename (src, dst)

Renommez le fichier ou le répertoire src en dst. ... Sous Unix, si dst existe et est un fichier, il sera remplacé silencieusement si l'utilisateur a l'autorisation. L'opération peut échouer sur certaines versions Unix si src et dst sont sur des systèmes de fichiers différents. En cas de succès, le changement de nom sera une opération atomique (il s'agit d'une exigence POSIX). Sous Windows, si dst existe déjà, OSError sera déclenché ( https://docs.python.org/2/library/os.html#os.rename )

Le remplacement silencieux est le problème. Sous Linux. Le "si dst existe déjà, OSError sera levé" est idéal pour mes besoins. Mais uniquement sous Windows, malheureusement.

Je suppose que l'exemple de parity3 fonctionne toujours la plupart du temps, en raison de sa condition if

if not os.path.exists(lock_filename):
    try:
        os.rename(tmp_filename,lock_filename)

Mais alors le tout n'est plus atomique.

Parce que la condition if peut être vraie dans deux processus parallèles, puis les deux seront renommés, mais un seul gagnera la course de changement de nom. Et aucune exception levée (sous Linux).

Aucune suggestion? Merci!

P.S .: Je sais que ce n'est pas la bonne façon, mais il me manque une alternative. VEUILLEZ ne pas me punir en diminuant ma réputation. J'ai beaucoup regardé autour de moi pour résoudre ce problème moi-même. Comment PM utilisateurs ici? Et meh pourquoi je ne peux pas?

3
akrueger

Voici un début sur la moitié win32 d'une implémentation portable, qui n'a pas besoin d'un mécanisme de verrouillage séparé.

Nécessite Python pour les extensions Windows pour passer à l'api win32, mais c'est à peu près obligatoire pour python sur Windows déjà, et peut également être fait avec ctypes . Le code pourrait être adapté pour exposer plus de fonctionnalités si nécessaire (comme autoriser FILE_SHARE_READ plutôt que pas de partage du tout). Voir aussi la documentation MSDN pour les appels système CreateFile et WriteFile et article sur la création et l'ouverture de fichiers .

Comme cela a été mentionné, vous pouvez utiliser le module standard fcntl pour implémenter la moitié unix de cela, si nécessaire.

import winerror, pywintypes, win32file

class LockError(StandardError):
    pass

class WriteLockedFile(object):
    """
    Using win32 api to achieve something similar to file(path, 'wb')
    Could be adapted to handle other modes as well.
    """
    def __init__(self, path):
        try:
            self._handle = win32file.CreateFile(
                path,
                win32file.GENERIC_WRITE,
                0,
                None,
                win32file.OPEN_ALWAYS,
                win32file.FILE_ATTRIBUTE_NORMAL,
                None)
        except pywintypes.error, e:
            if e[0] == winerror.ERROR_SHARING_VIOLATION:
                raise LockError(e[2])
            raise
    def close(self):
        self._handle.close()
    def write(self, str):
        win32file.WriteFile(self._handle, str)

Voici comment se comporte votre exemple ci-dessus:

>>> path = "C:\\scr.txt"
>>> file1 = WriteLockedFile(path)
>>> file2 = WriteLockedFile(path) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
    ...
LockError: ...
>>> file1.write("111")
>>> file1.close()
>>> print file(path).read()
111
3
gz.

En supposant que votre interpréteur Python, et le système d'exploitation et le système de fichiers sous-jacents traitent os.rename comme une opération atomique et qu'il produira une erreur lorsque la destination existe, la méthode suivante est exempte de conditions de concurrence. J'utilise ceci en production sur une machine linux. Ne nécessite aucune bibliothèque tierce et ne dépend pas du système d'exploitation, et à part une création de fichier supplémentaire, le résultat de performance est acceptable pour de nombreux cas d'utilisation. Vous pouvez facilement appliquer le modèle de décorateur de fonction de python ou une 'with_statement' contextmanager ici pour résumer le gâchis.

Vous devrez vous assurer que lock_filename n'existe pas avant le début d'un nouveau processus/tâche.

import os,time
def get_tmp_file():
    filename='tmp_%s_%s'%(os.getpid(),time.time())
    open(filename).close()
    return filename

def do_exclusive_work():
    print 'exclusive work being done...'

num_tries=10
wait_time=10
lock_filename='filename.lock'
acquired=False
for try_num in xrange(num_tries):
    tmp_filename=get_tmp_file()
    if not os.path.exists(lock_filename):
        try:
            os.rename(tmp_filename,lock_filename)
            acquired=True
        except (OSError,ValueError,IOError), e:
            pass
    if acquired:
        try:
            do_exclusive_work()
        finally:
            os.remove(lock_filename)
        break
    os.remove(tmp_filename)
    time.sleep(wait_time)
assert acquired, 'maximum tries reached, failed to acquire lock file'

[~ # ~] modifier [~ # ~]

Il est apparu que os.rename remplace silencieusement la destination sur un système d'exploitation non Windows. Merci de l'avoir signalé @ akrueger!

Voici une solution de contournement, provenant de ici :

Au lieu d'utiliser os.rename, vous pouvez utiliser:

try:
    if os.name != 'nt': # non-windows needs a create-exclusive operation
        fd = os.open(lock_filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL)
        os.close(fd)
    # non-windows os.rename will overwrite lock_filename silently.
    # We leave this call in here just so the tmp file is deleted but it could be refactored so the tmp file is never even generated for a non-windows OS
    os.rename(tmp_filename,lock_filename)
    acquired=True
except (OSError,ValueError,IOError), e:
    if os.name != 'nt' and not 'File exists' in str(e): raise

@ akrueger Vous êtes probablement très bien avec votre solution basée sur les répertoires, vous donnant simplement une autre méthode.

2
parity3

Je préfère utiliser filelock , une bibliothèque multiplateforme Python bibliothèque qui nécessite à peine tout code supplémentaire. Voici un exemple de la façon de l'utiliser:

from filelock import FileLock

lockfile = r"c:\scr.txt"
lock = FileLock(lockfile + ".lock")
with lock:
    file = open(path, "w")
    file.write("111")
    file.close()

Tout code dans le with lock: block est thread-safe, ce qui signifie qu'il sera terminé avant qu'un autre processus n'ait accès au fichier.

0
Josh Correia

Pour vous protéger lors de l'ouverture de fichiers dans une seule application, vous pouvez essayer quelque chose comme ceci:

import time
class ExclusiveFile(file):
    openFiles = {}
    fileLocks = []

    class FileNotExclusiveException(Exception):
        pass

    def __init__(self, *args):

        sMode = 'r'
        sFileName = args[0]
        try:
            sMode = args[1]
        except:
            pass
        while sFileName in ExclusiveFile.fileLocks:
            time.sleep(1)

        ExclusiveFile.fileLocks.append(sFileName)

        if not sFileName in ExclusiveFile.openFiles.keys() or (ExclusiveFile.openFiles[sFileName] == 'r' and sMode == 'r'):
            ExclusiveFile.openFiles[sFileName] = sMode
            try:
                file.__init__(self, sFileName, sMode)
            finally:
                ExclusiveFile.fileLocks.remove(sFileName)
         else:
            ExclusiveFile.fileLocks.remove(sFileName)
            raise self.FileNotExclusiveException(sFileName)

    def close(self):
        del ExclusiveFile.openFiles[self.name]
        file.close(self)

De cette façon, vous sous-classe la classe file. Maintenant, faites simplement:

>>> f = ExclusiveFile('/tmp/a.txt', 'r')
>>> f
<open file '/tmp/a.txt', mode 'r' at 0xb7d7cc8c>
>>> f1 = ExclusiveFile('/tmp/a.txt', 'r')
>>> f1
<open file '/tmp/a.txt', mode 'r' at 0xb7d7c814>
>>> f2 = ExclusiveFile('/tmp/a.txt', 'w') # can't open it for writing now
exclfile.FileNotExclusiveException: /tmp/a.txt

Si vous l'ouvrez d'abord avec le mode 'w', il ne permettra plus d'ouvrir, même en mode lecture, comme vous le vouliez ...

0
kender