Pregunta ¿Cuál es la mejor manera de abrir un archivo para acceso exclusivo en Python?


¿Cuál es la forma más elegante de resolver esto?

  • abra un archivo para leer, pero solo si no está abierto para escribir
  • abra un archivo para escribir, pero solo si no está abierto para leer o escribir

Las funciones integradas funcionan así

>>> 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 ahora contiene '111'.

>>> file2.close()

scr.txt fue sobrescrito y ahora contiene '222' (en Windows, Python 2.4).

La solución debería funcionar dentro del mismo proceso (como en el ejemplo anterior) y también cuando otro proceso haya abierto el archivo.
Se prefiere, si un programa bloqueado no mantiene el bloqueo abierto.


32
2017-10-09 06:58


origen


Respuestas:


No creo que haya una forma completamente cruzada. En Unix, el módulo fcntl hará esto por usted. Sin embargo, en Windows (que supongo que es por las rutas), tendrá que utilizar el módulo win32file.

Afortunadamente, hay una implementación portátil (portalocker) utilizando el método apropiado de plataforma en el libro de cocina de pitón.

Para usarlo, abra el archivo, y luego llame:

portalocker.lock(file, flags)

donde los indicadores son portalocker.LOCK_EX para acceso de escritura exclusivo, o LOCK_SH para acceso de lectura compartido.


20
2017-10-09 09:00



La solución debería funcionar dentro del mismo proceso (como en el ejemplo anterior) y también cuando otro proceso haya abierto el archivo.

Si con 'otro proceso' quiere decir 'cualquier proceso' (es decir, no su programa), en Linux no hay forma de lograr esto basándose únicamente en las llamadas al sistema (fcntl & amigos). Lo que quieres es bloqueo obligatorio, y la forma de Linux para obtenerlo está un poco más involucrada:

Reinstala la partición que contiene tu archivo con el mando opción:

# mount -o remount,mand /dev/hdXY

Selecciona el sgid bandera para su archivo:

# chmod g-x,g+s yourfile

En su código de Python, obtenga un bloqueo exclusivo en ese archivo:

fcntl.flock(fd, fcntl.LOCK_EX)

Ahora incluso gato no podrá leer el archivo hasta que suelte el bloqueo.


8
2017-10-12 02:46



Aquí hay un comienzo en la mitad de una implementación portátil de win32, que no necesita un mecanismo de bloqueo separado.

Requiere el Python para extensiones de Windows para llegar a la API de Win32, pero eso ya es bastante obligatorio para Python en Windows, y alternativamente puede hacerse con ctypes. El código podría adaptarse para exponer más funcionalidades si es necesario (como permitir FILE_SHARE_READ en lugar de no compartir nada). Consulte también la documentación de MSDN para CreateFile y WriteFile llamadas al sistema, y artículo sobre cómo crear y abrir archivos.

Como se ha mencionado, puede usar el estándar fcntl módulo para implementar la mitad de esto de unix, si es necesario.

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)

Así es como se comporta su ejemplo anterior:

>>> 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
2017-10-09 19:30



EDITAR: ¡Lo resolví yo mismo! Mediante el uso existencia de directorio y la edad como un mecanismo de bloqueo! Bloquear por archivo es seguro solo en Windows (porque Linux sobrescribe silenciosamente), pero el bloqueo por directorio funciona perfectamente tanto en Linux como en Windows. Ver mi GIT donde creé una clase fácil de usar 'lockbydir.DLock' para eso:

https://github.com/drandreaskrueger/lockbydir

En la parte inferior del archivo léame, encontrarás 3 GITplayers donde puedes ver los ejemplos de código ejecutados en vivo en tu navegador. Genial, ¿no? :-)

Gracias por tu atención


Esta fue mi pregunta original:

Me gustaría responder a la paridad3 (https://meta.stackoverflow.com/users/1454536/parity3) pero no puedo comentar directamente ('Debes tener 50 reputación para comentar'), ni veo ninguna forma de contactarlo directamente. ¿Qué me sugieres para llegar a él?

Mi pregunta:

He implementado algo similar a lo que parity3 sugirió aquí como respuesta: https://stackoverflow.com/a/21444311/3693375 ("Asumiendo tu intérprete de Python, y el ...")

Y funciona brillantemente - en Windows. (Lo estoy usando para implementar un mecanismo de bloqueo que funciona en procesos iniciados independientemente). https://github.com/drandreaskrueger/lockbyfile )

Pero aparte de parity3 dice, NO funciona igual en Linux:

os.rename (src, dst)

Cambie el nombre del archivo o directorio src a dst. ... En Unix, si existe dst   y es un archivo,   será reemplazado silenciosamente si el usuario tiene permiso.   La operación puede fallar en algunos sabores de Unix si src y dst   están en diferentes sistemas de archivos. Si tiene éxito, el cambio de nombre se   ser una operación atómica (este es un requisito POSIX).   En Windows, si ya existe, OSError se levantará   (https://docs.python.org/2/library/os.html#os.rename)

El reemplazo silencioso es el problema. En Linux. El "si ya existe dst, OSError se levantará" es ideal para mis propósitos. Pero solo en Windows, lamentablemente.

Supongo que el ejemplo de parity3 sigue funcionando la mayor parte del tiempo, debido a su condición if

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

Pero luego todo el asunto ya no es atómico.

Porque la condición if podría ser verdadera en dos procesos paralelos, y luego ambos cambiarán de nombre, pero solo uno ganará la carrera de cambio de nombre. Y ninguna excepción planteada (en Linux).

¿Alguna sugerencia? ¡Gracias!

P.S .: Sé que esta no es la manera correcta, pero me falta una alternativa. POR FAVOR, no me castigue con rebajar mi reputación. Miré a mi alrededor, para resolverlo yo mismo. ¿Cómo PM a los usuarios aquí? Y Meh ¿Por qué no puedo?


2
2018-02-15 23:41



Para que estés seguro al abrir archivos dentro de una aplicación, puedes intentar algo como esto:

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 esta manera subclasifica el file clase. Ahora solo hazlo:

>>> 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 lo abres primero con el modo 'w', no permitirá más aperturas, incluso en modo lectura, tal como querías ...


1
2017-10-09 07:48



Suponiendo que su intérprete de Python, y el sistema operativo y el sistema de archivos subyacentes tratan a os.rename como una operación atómica y se producirá un error cuando existe el destino, el siguiente método está libre de condiciones de carrera. Estoy usando esto en producción en una máquina Linux. No requiere libs de terceros y no depende del sistema operativo, y aparte de crear un archivo adicional, el rendimiento alcanzado es aceptable para muchos casos de uso. Puede aplicar fácilmente el patrón de decorador de funciones de python o un gestor de contexto 'with_statement' aquí para abstraer el desorden.

Tendrá que asegurarse de que lock_filename no exista antes de que comience un nuevo proceso / tarea.

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'

EDITAR

Ha salido a la luz que os.rename sobrescribe silenciosamente el destino en un sistema operativo que no es de Windows. Gracias por señalar esto @ akrueger!

Aquí hay una solución, recopilada de aquí:

En lugar de usar os.rename puedes usar:

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 Probablemente estés bien con tu solución basada en directorio, simplemente dándote un método alternativo.


1
2018-01-29 22:45