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