Каков наилучший способ открыть файл для эксклюзивного доступа в Python?

Какой самый элегантный способ решить эту проблему:

  • открыть файл для чтения, но только если он еще не открыт для записи
  • открыть файл для записи, но только если он еще не открыт для чтения или записи

Встроенные функции работают следующим образом

>>> 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 теперь содержит '111'.

>>> file2.close()

scr.txt был перезаписан и теперь содержит "222" (в Windows, Python 2.4).

Решение должно работать внутри одного и того же процесса (например, в примере выше), а также когда другой процесс открыл файл.
Предпочтительно, если программа сбоя не будет блокировать блокировку.

Ответы

Ответ 1

Я не думаю, что есть полностью кроссплатформенный способ. В unix модуль fcntl сделает это за вас. Однако в окнах (которые я предполагаю, что вы по пути) вам нужно использовать модуль win32file.

К счастью, есть переносимая реализация (portalocker) с использованием подходящего метода платформы в кулинарной книге python.

Чтобы использовать его, откройте файл, а затем вызовите:

portalocker.lock(file, flags)

где flags - portalocker.LOCK_EX для исключительного доступа на запись или LOCK_SH для общего доступа к чтению.

Ответ 2

Решение должно работать внутри одного и того же процесса (например, в примере выше), а также когда другой процесс открыл файл.

Если по "другому процессу" вы подразумеваете "какой бы то ни было процесс" (т.е. не ваша программа), в Linux нет способа сделать это, полагаясь только на системные вызовы (fcntl и друзья). Вы хотите обязательную блокировку, а способ Linux для ее получения немного более активен:

Перезагрузите раздел, содержащий файл с параметром mand:

# mount -o remount,mand /dev/hdXY

Установите флаг sgid для вашего файла:

# chmod g-x,g+s yourfile

В вашем коде Python получите эксклюзивную блокировку этого файла:

fcntl.flock(fd, fcntl.LOCK_EX)

Теперь даже кошка не сможет прочитать файл, пока вы не отпустите блокировку.

Ответ 3

Здесь начинается на половине win32 портативной реализации, которая не нуждается в отдельном механизме блокировки.

Требуется Python для Windows Extensions, чтобы перейти к win32 api, но это в значительной степени обязательное для python на окнах уже и может альтернативно можно сделать с помощью ctypes. Код может быть адаптирован для предоставления большей функциональности, если это необходимо (например, разрешение FILE_SHARE_READ, а не отсутствие общего доступа). См. Также документацию MSDN для CreateFile и WriteFile и статья о создании и открытии файлов.

Как уже упоминалось, вы можете использовать стандартный fcntl модуль, чтобы реализовать половину этого раздела, если это необходимо.

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)

Вот как ведет себя ваш пример сверху:

>>> 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

Ответ 4

EDIT: Я решил это сам! Используя существование каталога и возраст как механизм блокировки! Блокировка по файлу безопасна только в Windows (потому что Linux молча перезаписывает), но блокировка по каталогу прекрасно работает как в Linux, так и в Windows. См. Мой GIT, где я создал простой в использовании класс 'lockbydir.DLock' для этого:

https://github.com/drandreaskrueger/lockbydir

В нижней части readme вы найдете 3 GITplayers, где вы можете увидеть примеры кода, выполняемые в прямом эфире в вашем браузере! Довольно круто, не так ли?: -)

Спасибо за внимание


Это был мой оригинальный вопрос:

Я хотел бы ответить на parity3 (https://meta.stackoverflow.com/users/1454536/parity3), но я не могу напрямую комментировать ( "У вас должно быть 50 репутации для комментариев" ), и я не могу см. любой способ связаться с ним напрямую. Что вы предлагаете мне, чтобы пройти к нему?

Мой вопрос:

Я реализовал что-то похожее на то, что parity3 предложил здесь как ответ: fooobar.com/questions/989/... ( "Предполагая, что ваш интерпретатор Python и..." )

И он работает блестяще - на Windows. (Я использую его для реализации механизма блокировки, который работает через независимо запущенные процессы. https://github.com/drandreaskrueger/lockbyfile)

Но, кроме определения четности3, он не работает в Linux:

os.rename(src, dst)

Переименуйте файл или каталог src в dst.... В Unix, если dst существует и является файлом, он будет заменен молча, если у пользователя есть разрешение. Операция может завершиться неудачей при некоторых вариантах Unix, если src и dst находятся в разных файловых системах. В случае успеха переименование быть атомной операцией (это требование POSIX). В Windows, если dst уже существует, OSError будет поднят (https://docs.python.org/2/library/os.html#os.rename)

Бесшумная замена - проблема. В Linux. "Если dst уже существует, OSError будет поднят" отлично подходит для моих целей. Но только в Windows, к сожалению.

Я думаю, что пример parity3 все еще работает большую часть времени из-за его if

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

Но тогда все это не является атомарным.

Поскольку условие if может быть истинным в двух параллельных процессах, а затем оба будут переименовываться, но только один победит в переименовании. И никаких исключений не возникало (в Linux).

Любые предложения? Благодарю!

P.S.: Я знаю, что это не правильно, но мне не хватает альтернативы. ПОЖАЛУЙСТА, не наказывайте меня, понижая мою репутацию. Я много огляделся, чтобы решить это сам. Как PM пользователей здесь? И почему я не могу?

Ответ 5

Чтобы сделать вас безопасным при открытии файлов в одном приложении, вы можете попробовать что-то вроде этого:

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)

Таким образом вы подклассифицируете класс file. Теперь просто выполните:

>>> 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

Если вы сначала откроете его с помощью режима "w", он больше не откроется, даже в режиме чтения, как вы хотели...

Ответ 6

Предполагая, что ваш интерпретатор Python и базовые os и файловая система обрабатывают os.rename как атомную операцию, и она будет ошибочной, когда существует назначение, следующий метод свободен от условий гонки. Я использую это в производстве на Linux-машине. Не требует сторонних библиотек и не зависит от ОС, и помимо создания дополнительного файла, производительность считается приемлемой для многих случаев использования. Вы можете легко применить шаблон декоратора функции python или контекстный менеджер 'with_statement' здесь, чтобы абстрагироваться от беспорядка.

Вам нужно убедиться, что lock_filename не существует до начала нового процесса/задачи.

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'

ИЗМЕНИТЬ

Выяснилось, что os.rename молча перезаписывает место назначения в операционной системе, отличной от Windows. Спасибо, что указали это на @akrueger!

Вот обходной путь, собранный из здесь:

Вместо использования os.rename вы можете использовать:

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 Вы, вероятно, просто отлично справляетесь со своим решением на основе каталогов, просто предоставляя альтернативный метод.